<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.4.3/angular.js" data-semver="1.4.3"></script>
    <script src="app.js"></script>
  </head>

  <body ng-init="longtext = 'Long text - make shorter'">
    
    <h3>Regular tooltip</h3>
    <span tooltip tooltip-message="'Hello World!'">Hover over me!</span>
    
    <h3>Tooltip that optionally shows if content overflows</h3>
    <p style="max-width: 100px;" tooltip tooltip-message="longtext" tooltip-overflow>{{longtext}}</p>
    <input ng-model="longtext">
  </body>

</html>

.tooltip {
  position: absolute;
  padding: 2px;
  background: rgba(220, 220, 220, 0.8);
  border: 1px solid rgb(128, 128, 128);
  border-radius: 3px;
}

[tooltip-overflow] {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
(function () {
  var app = angular.module('plunker', []);
  
  function TooltipModel (config) {
    // Boolean - if the tooltip should be shown - component decorators can modify this property.
    this.shouldShow = config.shouldShow || true;
    
    // Boolean - if the tooltip is currently being shown. Component decorators can use this property to test if the tooltip is being shown
    this.isShowing = config.isShowing || false;
    
    // String - the message the tooltip contains. It is an angular expression
    this.message = config.message || '';
    
    // Number - the x coordinate of the tooltip
    this.x = config.x || 0;
    
    // Number - the y coordinate of the tooltip
    this.y = config.y || 0;
  }
  
  function TooltipController () {
  
    // API for state
    this.model = new TooltipModel({});
  }
  
  TooltipController.prototype.show = function showTooltip () {
    if (this.model.shouldShow) {
      this.model.isShowing = true;
    }
  };
  
  TooltipController.prototype.hide = function hideTooltip () {
    this.model.isShowing = false;
  }
  
  TooltipController.prototype.setPosition = function setPosition (x, y) {
    this.model.x = x;
    this.model.y = y;
  }
  app.controller('TooltipController', TooltipController);

  // Tooltip directive
  app.directive('tooltip', function($compile) {
    return {
      restrict: 'A',
      controller: 'TooltipController',
      // controllerAs: 'tooltip', // we won't use this since we are kind of stepping outside normal angular stuff
  
      link: function tooltipLink ($scope, $element, $attrs, TooltipController) {
        var $body = angular.element(document.body);
        var $tooltipElement;
        var tooltipScope = $scope.$new(true); // new isolate scope
        tooltipScope.tooltip = TooltipController; // controllerAs in the isolate scope
  
        // events
        $element.on('mouseover', function onMouseover (event) {
          TooltipController.setPosition(event.clientX + 10, event.clientY + 10);
          TooltipController.show();
          tooltipScope.$digest(); // let Angular know something interesting happened - local digest for performance
        });
  
        $element.on('mouseout', function onMouseout (event) {
          TooltipController.hide();
          tooltipScope.$digest(); // let Angular know something interesting happened - local digest for performance
        });
  
        // react to state changes
        tooltipScope.$watch('tooltip.model.isShowing', function (isShowing) {
          if (isShowing) {
            // lazy initialization of tooltip contents
            if (!$tooltipElement) {
              $tooltipElement = $compile('<div class="tooltip">{{tooltip.model.message}}</div>')(tooltipScope);
            }
            $tooltipElement.css({
              top: TooltipController.model.y + 'px',
              left: TooltipController.model.x + 'px'
            });
            $body.append($tooltipElement);
          } else {
            $tooltipElement && $tooltipElement.remove();
          }
        });
      }
    };
  });
  
  app.directive('tooltipMessage', function () {
    return {
      restrict: 'A',
      require: 'tooltip',
      
      link: function tooltipMessageLink ($scope, $element, $attrs, TooltipController) {
        $scope.$watch($attrs.tooltipMessage, function (message) {
          TooltipController.model.message = message;
        });
      }
    }
  });
  
  app.directive('tooltipOverflow', function () {
    return {
      restrict: 'A',
      require: 'tooltip',
      
      link: function tooltipOverflowLink ($scope, $element, $attrs, TooltipController) {
        $element.on('mouseover', function (event) {
          if ($element[0].scrollWidth > $element[0].clientWidth) {
            TooltipController.model.shouldShow = true;
          } else {
            TooltipController.model.shouldShow = false;
          }
        });
      }
    }
  });

})();