tooltip.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. (function( window, angular, undefined ){
  8. "use strict";
  9. /**
  10. * @ngdoc module
  11. * @name material.components.tooltip
  12. */
  13. angular
  14. .module('material.components.tooltip', [ 'material.core' ])
  15. .directive('mdTooltip', MdTooltipDirective);
  16. /**
  17. * @ngdoc directive
  18. * @name mdTooltip
  19. * @module material.components.tooltip
  20. * @description
  21. * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
  22. *
  23. * Place a `<md-tooltip>` as a child of the element it describes.
  24. *
  25. * A tooltip will activate when the user focuses, hovers over, or touches the parent.
  26. *
  27. * @usage
  28. * <hljs lang="html">
  29. * <md-button class="md-fab md-accent" aria-label="Play">
  30. * <md-tooltip>
  31. * Play Music
  32. * </md-tooltip>
  33. * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
  34. * </md-button>
  35. * </hljs>
  36. *
  37. * @param {expression=} md-visible Boolean bound to whether the tooltip is
  38. * currently visible.
  39. * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 300ms.
  40. * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
  41. * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
  42. */
  43. function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
  44. $animate, $q) {
  45. var TOOLTIP_SHOW_DELAY = 300;
  46. var TOOLTIP_WINDOW_EDGE_SPACE = 8;
  47. return {
  48. restrict: 'E',
  49. transclude: true,
  50. priority:210, // Before ngAria
  51. template: '<div class="md-background"></div>' +
  52. '<div class="md-content" ng-transclude></div>',
  53. scope: {
  54. visible: '=?mdVisible',
  55. delay: '=?mdDelay',
  56. autohide: '=?mdAutohide'
  57. },
  58. link: postLink
  59. };
  60. function postLink(scope, element, attr) {
  61. $mdTheming(element);
  62. var parent = getParentWithPointerEvents(),
  63. background = angular.element(element[0].getElementsByClassName('md-background')[0]),
  64. content = angular.element(element[0].getElementsByClassName('md-content')[0]),
  65. direction = attr.mdDirection,
  66. current = getNearestContentElement(),
  67. tooltipParent = angular.element(current || document.body),
  68. debouncedOnResize = $$rAF.throttle(function () { if (scope.visible) positionTooltip(); });
  69. // Initialize element
  70. setDefaults();
  71. manipulateElement();
  72. bindEvents();
  73. configureWatchers();
  74. addAriaLabel();
  75. function setDefaults () {
  76. if (!angular.isDefined(attr.mdDelay)) scope.delay = TOOLTIP_SHOW_DELAY;
  77. }
  78. function configureWatchers () {
  79. scope.$on('$destroy', function() {
  80. scope.visible = false;
  81. element.remove();
  82. angular.element($window).off('resize', debouncedOnResize);
  83. });
  84. scope.$watch('visible', function (isVisible) {
  85. if (isVisible) showTooltip();
  86. else hideTooltip();
  87. });
  88. }
  89. function addAriaLabel () {
  90. if (!parent.attr('aria-label') && !parent.text().trim()) {
  91. parent.attr('aria-label', element.text().trim());
  92. }
  93. }
  94. function manipulateElement () {
  95. element.detach();
  96. element.attr('role', 'tooltip');
  97. }
  98. /**
  99. * Scan up dom hierarchy for enabled parent;
  100. */
  101. function getParentWithPointerEvents () {
  102. var parent = element.parent();
  103. // jqLite might return a non-null, but still empty, parent; so check for parent and length
  104. while (hasComputedStyleValue('pointer-events','none', parent)) {
  105. parent = parent.parent();
  106. }
  107. return parent;
  108. }
  109. function getNearestContentElement () {
  110. var current = element.parent()[0];
  111. // Look for the nearest parent md-content, stopping at the rootElement.
  112. while (current && current !== $rootElement[0] && current !== document.body) {
  113. current = current.parentNode;
  114. }
  115. return current;
  116. }
  117. function hasComputedStyleValue(key, value, target) {
  118. var hasValue = false;
  119. if ( target && target.length ) {
  120. key = attr.$normalize(key);
  121. target = target[0] || element[0];
  122. var computedStyles = $window.getComputedStyle(target);
  123. hasValue = angular.isDefined(computedStyles[key]) && (computedStyles[key] == value);
  124. }
  125. return hasValue;
  126. }
  127. function bindEvents () {
  128. var mouseActive = false;
  129. var ngWindow = angular.element($window);
  130. // Store whether the element was focused when the window loses focus.
  131. var windowBlurHandler = function() {
  132. elementFocusedOnWindowBlur = document.activeElement === parent[0];
  133. };
  134. var elementFocusedOnWindowBlur = false;
  135. ngWindow.on('blur', windowBlurHandler);
  136. scope.$on('$destroy', function() {
  137. ngWindow.off('blur', windowBlurHandler);
  138. });
  139. var enterHandler = function(e) {
  140. // Prevent the tooltip from showing when the window is receiving focus.
  141. if (e.type === 'focus' && elementFocusedOnWindowBlur) {
  142. elementFocusedOnWindowBlur = false;
  143. return;
  144. }
  145. parent.on('blur mouseleave touchend touchcancel', leaveHandler );
  146. setVisible(true);
  147. };
  148. var leaveHandler = function () {
  149. var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
  150. if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) {
  151. parent.off('blur mouseleave touchend touchcancel', leaveHandler );
  152. parent.triggerHandler("blur");
  153. setVisible(false);
  154. }
  155. mouseActive = false;
  156. };
  157. // to avoid `synthetic clicks` we listen to mousedown instead of `click`
  158. parent.on('mousedown', function() { mouseActive = true; });
  159. parent.on('focus mouseenter touchstart', enterHandler );
  160. angular.element($window).on('resize', debouncedOnResize);
  161. }
  162. function setVisible (value) {
  163. setVisible.value = !!value;
  164. if (!setVisible.queued) {
  165. if (value) {
  166. setVisible.queued = true;
  167. $timeout(function() {
  168. scope.visible = setVisible.value;
  169. setVisible.queued = false;
  170. }, scope.delay);
  171. } else {
  172. $mdUtil.nextTick(function() { scope.visible = false; });
  173. }
  174. }
  175. }
  176. function showTooltip() {
  177. // Insert the element before positioning it, so we can get the position
  178. // and check if we should display it
  179. tooltipParent.append(element);
  180. // Check if we should display it or not.
  181. // This handles hide-* and show-* along with any user defined css
  182. if ( hasComputedStyleValue('display','none') ) {
  183. scope.visible = false;
  184. element.detach();
  185. return;
  186. }
  187. positionTooltip();
  188. angular.forEach([element, background, content], function (element) {
  189. $animate.addClass(element, 'md-show');
  190. });
  191. }
  192. function hideTooltip() {
  193. var promises = [];
  194. angular.forEach([element, background, content], function (it) {
  195. if (it.parent() && it.hasClass('md-show')) {
  196. promises.push($animate.removeClass(it, 'md-show'));
  197. }
  198. });
  199. $q.all(promises)
  200. .then(function () {
  201. if (!scope.visible) element.detach();
  202. });
  203. }
  204. function positionTooltip() {
  205. var tipRect = $mdUtil.offsetRect(element, tooltipParent);
  206. var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
  207. var newPosition = getPosition(direction);
  208. // If the user provided a direction, just nudge the tooltip onto the screen
  209. // Otherwise, recalculate based on 'top' since default is 'bottom'
  210. if (direction) {
  211. newPosition = fitInParent(newPosition);
  212. } else if (newPosition.top > element.prop('offsetParent').scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
  213. newPosition = fitInParent(getPosition('top'));
  214. }
  215. element.css({top: newPosition.top + 'px', left: newPosition.left + 'px'});
  216. positionBackground();
  217. function positionBackground () {
  218. var size = direction === 'left' || direction === 'right'
  219. ? Math.sqrt(Math.pow(tipRect.width, 2) + Math.pow(tipRect.height / 2, 2)) * 2
  220. : Math.sqrt(Math.pow(tipRect.width / 2, 2) + Math.pow(tipRect.height, 2)) * 2,
  221. position = direction === 'left' ? { left: 100, top: 50 }
  222. : direction === 'right' ? { left: 0, top: 50 }
  223. : direction === 'top' ? { left: 50, top: 100 }
  224. : { left: 50, top: 0 };
  225. background.css({
  226. width: size + 'px',
  227. height: size + 'px',
  228. left: position.left + '%',
  229. top: position.top + '%'
  230. });
  231. }
  232. function fitInParent (pos) {
  233. var newPosition = { left: pos.left, top: pos.top };
  234. newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
  235. newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
  236. newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
  237. newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE );
  238. return newPosition;
  239. }
  240. function getPosition (dir) {
  241. return dir === 'left'
  242. ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
  243. top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
  244. : dir === 'right'
  245. ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
  246. top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
  247. : dir === 'top'
  248. ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
  249. top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
  250. : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
  251. top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
  252. }
  253. }
  254. }
  255. }
  256. MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q"];
  257. })(window, window.angular);