2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.tooltip');
8 goog.require('ng.material.core');
11 * @name material.components.tooltip
14 .module('material.components.tooltip', [ 'material.core' ])
15 .directive('mdTooltip', MdTooltipDirective);
20 * @module material.components.tooltip
22 * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
24 * Place a `<md-tooltip>` as a child of the element it describes.
26 * A tooltip will activate when the user focuses, hovers over, or touches the parent.
30 * <md-button class="md-fab md-accent" aria-label="Play">
34 * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
38 * @param {expression=} md-visible Boolean bound to whether the tooltip is
40 * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 400ms.
41 * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
42 * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
44 function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
47 var TOOLTIP_SHOW_DELAY = 300;
48 var TOOLTIP_WINDOW_EDGE_SPACE = 8;
53 priority:210, // Before ngAria
55 <div class="md-background"></div>\
56 <div class="md-content" ng-transclude></div>',
58 visible: '=?mdVisible',
60 autohide: '=?mdAutohide'
65 function postLink(scope, element, attr) {
69 var parent = getParentWithPointerEvents(),
70 background = angular.element(element[0].getElementsByClassName('md-background')[0]),
71 content = angular.element(element[0].getElementsByClassName('md-content')[0]),
72 direction = attr.mdDirection,
73 current = getNearestContentElement(),
74 tooltipParent = angular.element(current || document.body),
75 debouncedOnResize = $$rAF.throttle(function () { if (scope.visible) positionTooltip(); });
87 function setDefaults () {
88 if (!angular.isDefined(attr.mdDelay)) scope.delay = TOOLTIP_SHOW_DELAY;
91 function configureWatchers () {
92 scope.$on('$destroy', function() {
93 scope.visible = false;
95 angular.element($window).off('resize', debouncedOnResize);
97 scope.$watch('visible', function (isVisible) {
98 if (isVisible) showTooltip();
103 function addAriaLabel () {
104 if (!parent.attr('aria-label') && !parent.text().trim()) {
105 parent.attr('aria-label', element.text().trim());
109 function manipulateElement () {
111 element.attr('role', 'tooltip');
114 function getParentWithPointerEvents () {
115 var parent = element.parent();
116 while (parent && $window.getComputedStyle(parent[0])['pointer-events'] == 'none') {
117 parent = parent.parent();
122 function getNearestContentElement () {
123 var current = element.parent()[0];
124 // Look for the nearest parent md-content, stopping at the rootElement.
125 while (current && current !== $rootElement[0] && current !== document.body) {
126 current = current.parentNode;
131 function hasComputedStyleValue(key, value) {
132 // Check if we should show it or not...
133 var computedStyles = $window.getComputedStyle(element[0]);
134 return angular.isDefined(computedStyles[key]) && (computedStyles[key] == value);
137 function bindEvents () {
138 var mouseActive = false;
139 var enterHandler = function() {
140 if (!hasComputedStyleValue('pointer-events','none')) {
144 var leaveHandler = function () {
145 var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
146 if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) {
152 // to avoid `synthetic clicks` we listen to mousedown instead of `click`
153 parent.on('mousedown', function() { mouseActive = true; });
154 parent.on('focus mouseenter touchstart', enterHandler );
155 parent.on('blur mouseleave touchend touchcancel', leaveHandler );
158 angular.element($window).on('resize', debouncedOnResize);
161 function setVisible (value) {
162 setVisible.value = !!value;
163 if (!setVisible.queued) {
165 setVisible.queued = true;
166 $timeout(function() {
167 scope.visible = setVisible.value;
168 setVisible.queued = false;
171 $timeout(function() { scope.visible = false; });
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);
181 // Check if we should display it or not.
182 // This handles hide-* and show-* along with any user defined css
183 if ( hasComputedStyleValue('display','none') ) {
184 scope.visible = false;
190 angular.forEach([element, background, content], function (element) {
191 $animate.addClass(element, 'md-show');
195 function hideTooltip() {
197 $animate.removeClass(content, 'md-show'),
198 $animate.removeClass(background, 'md-show'),
199 $animate.removeClass(element, 'md-show')
200 ]).then(function () {
201 if (!scope.visible) element.detach();
205 function positionTooltip() {
206 var tipRect = $mdUtil.offsetRect(element, tooltipParent);
207 var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
208 var newPosition = getPosition(direction);
210 // If the user provided a direction, just nudge the tooltip onto the screen
211 // Otherwise, recalculate based on 'top' since default is 'bottom'
213 newPosition = fitInParent(newPosition);
214 } else if (newPosition.top > element.prop('offsetParent').scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
215 newPosition = fitInParent(getPosition('top'));
218 element.css({top: newPosition.top + 'px', left: newPosition.left + 'px'});
220 positionBackground();
222 function positionBackground () {
223 var size = direction === 'left' || direction === 'right'
224 ? Math.sqrt(Math.pow(tipRect.width, 2) + Math.pow(tipRect.height / 2, 2)) * 2
225 : Math.sqrt(Math.pow(tipRect.width / 2, 2) + Math.pow(tipRect.height, 2)) * 2,
226 position = direction === 'left' ? { left: 100, top: 50 }
227 : direction === 'right' ? { left: 0, top: 50 }
228 : direction === 'top' ? { left: 50, top: 100 }
229 : { left: 50, top: 0 };
233 left: position.left + '%',
234 top: position.top + '%'
238 function fitInParent (pos) {
239 var newPosition = { left: pos.left, top: pos.top };
240 newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
241 newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
242 newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
243 newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE );
247 function getPosition (dir) {
248 return dir === 'left'
249 ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
250 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
252 ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
253 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
255 ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
256 top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
257 : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
258 top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
265 MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q"];
267 ng.material.components.tooltip = angular.module("material.components.tooltip");