commonroutesApp/www/lib/angular-material/modules/js/tooltip/tooltip.js
www/lib/angular-material/modules/js/tooltip/tooltip.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10/**
11 * @ngdoc module
12 * @name material.components.tooltip
13 */
14MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q", "$interpolate"];
15angular
16 .module('material.components.tooltip', [ 'material.core' ])
17 .directive('mdTooltip', MdTooltipDirective);
18
19/**
20 * @ngdoc directive
21 * @name mdTooltip
22 * @module material.components.tooltip
23 * @description
24 * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
25 *
26 * Place a `<md-tooltip>` as a child of the element it describes.
27 *
28 * A tooltip will activate when the user focuses, hovers over, or touches the parent.
29 *
30 * @usage
31 * <hljs lang="html">
32 * <md-button class="md-fab md-accent" aria-label="Play">
33 * <md-tooltip>
34 * Play Music
35 * </md-tooltip>
36 * <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
37 * </md-button>
38 * </hljs>
39 *
40 * @param {expression=} md-visible Boolean bound to whether the tooltip is currently visible.
41 * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the
42 * parent. Defaults to 0ms on non-touch devices and 75ms on touch.
43 * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
44 * @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
45 */
46function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
47 $animate, $q, $interpolate) {
48
49 var ENTER_EVENTS = 'focus touchstart mouseenter';
50 var LEAVE_EVENTS = 'blur touchcancel mouseleave';
51 var SHOW_CLASS = 'md-show';
52 var TOOLTIP_SHOW_DELAY = 0;
53 var TOOLTIP_WINDOW_EDGE_SPACE = 8;
54
55 return {
56 restrict: 'E',
57 transclude: true,
58 priority: 210, // Before ngAria
59 template: '<div class="md-content _md" ng-transclude></div>',
60 scope: {
61 delay: '=?mdDelay',
62 visible: '=?mdVisible',
63 autohide: '=?mdAutohide',
64 direction: '@?mdDirection' // only expect raw or interpolated string value; not expression
65 },
66 compile: function(tElement, tAttr) {
67 if (!tAttr.mdDirection) {
68 tAttr.$set('mdDirection', 'bottom');
69 }
70
71 return postLink;
72 }
73 };
74
75 function postLink(scope, element, attr) {
76
77 $mdTheming(element);
78
79 var parent = $mdUtil.getParentWithPointerEvents(element),
80 content = angular.element(element[0].getElementsByClassName('md-content')[0]),
81 tooltipParent = angular.element(document.body),
82 showTimeout = null,
83 debouncedOnResize = $$rAF.throttle(function () { updatePosition(); });
84
85 if ($animate.pin) $animate.pin(element, parent);
86
87 // Initialize element
88
89 setDefaults();
90 manipulateElement();
91 bindEvents();
92
93 // Default origin transform point is 'center top'
94 // positionTooltip() is always relative to center top
95 updateContentOrigin();
96
97 configureWatchers();
98 addAriaLabel();
99
100
101 function setDefaults () {
102 scope.delay = scope.delay || TOOLTIP_SHOW_DELAY;
103 }
104
105 function updateContentOrigin() {
106 var origin = 'center top';
107 switch (scope.direction) {
108 case 'left' : origin = 'right center'; break;
109 case 'right' : origin = 'left center'; break;
110 case 'top' : origin = 'center bottom'; break;
111 case 'bottom': origin = 'center top'; break;
112 }
113 content.css('transform-origin', origin);
114 }
115
116 function onVisibleChanged (isVisible) {
117 if (isVisible) showTooltip();
118 else hideTooltip();
119 }
120
121 function configureWatchers () {
122 if (element[0] && 'MutationObserver' in $window) {
123 var attributeObserver = new MutationObserver(function(mutations) {
124 mutations
125 .forEach(function (mutation) {
126 if (mutation.attributeName === 'md-visible') {
127 if (!scope.visibleWatcher)
128 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
129 }
130 if (mutation.attributeName === 'md-direction') {
131 updatePosition(scope.direction);
132 }
133 });
134 });
135
136 attributeObserver.observe(element[0], { attributes: true });
137
138 // build watcher only if mdVisible is being used
139 if (attr.hasOwnProperty('mdVisible')) {
140 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
141 }
142 } else { // MutationObserver not supported
143 scope.visibleWatcher = scope.$watch('visible', onVisibleChanged );
144 scope.$watch('direction', updatePosition );
145 }
146
147 var onElementDestroy = function() {
148 scope.$destroy();
149 };
150
151 // Clean up if the element or parent was removed via jqLite's .remove.
152 // A couple of notes:
153 // - In these cases the scope might not have been destroyed, which is why we
154 // destroy it manually. An example of this can be having `md-visible="false"` and
155 // adding tooltips while they're invisible. If `md-visible` becomes true, at some
156 // point, you'd usually get a lot of inputs.
157 // - We use `.one`, not `.on`, because this only needs to fire once. If we were
158 // using `.on`, it would get thrown into an infinite loop.
159 // - This kicks off the scope's `$destroy` event which finishes the cleanup.
160 element.one('$destroy', onElementDestroy);
161 parent.one('$destroy', onElementDestroy);
162 scope.$on('$destroy', function() {
163 setVisible(false);
164 element.remove();
165 attributeObserver && attributeObserver.disconnect();
166 });
167
168 // Updates the aria-label when the element text changes. This watch
169 // doesn't need to be set up if the element doesn't have any data
170 // bindings.
171 if (element.text().indexOf($interpolate.startSymbol()) > -1) {
172 scope.$watch(function() {
173 return element.text().trim();
174 }, addAriaLabel);
175 }
176 }
177
178 function addAriaLabel (override) {
179 if ((override || !parent.attr('aria-label')) && !parent.text().trim()) {
180 var rawText = override || element.text().trim();
181 var interpolatedText = $interpolate(rawText)(parent.scope());
182 parent.attr('aria-label', interpolatedText);
183 }
184 }
185
186 function manipulateElement () {
187 element.detach();
188 element.attr('role', 'tooltip');
189 }
190
191 function bindEvents () {
192 var mouseActive = false;
193
194 // add an mutationObserver when there is support for it
195 // and the need for it in the form of viable host(parent[0])
196 if (parent[0] && 'MutationObserver' in $window) {
197 // use an mutationObserver to tackle #2602
198 var attributeObserver = new MutationObserver(function(mutations) {
199 if (mutations.some(function (mutation) {
200 return (mutation.attributeName === 'disabled' && parent[0].disabled);
201 })) {
202 $mdUtil.nextTick(function() {
203 setVisible(false);
204 });
205 }
206 });
207
208 attributeObserver.observe(parent[0], { attributes: true});
209 }
210
211 // Store whether the element was focused when the window loses focus.
212 var windowBlurHandler = function() {
213 elementFocusedOnWindowBlur = document.activeElement === parent[0];
214 };
215 var elementFocusedOnWindowBlur = false;
216
217 function windowScrollHandler() {
218 setVisible(false);
219 }
220
221 angular.element($window)
222 .on('blur', windowBlurHandler)
223 .on('resize', debouncedOnResize);
224
225 document.addEventListener('scroll', windowScrollHandler, true);
226 scope.$on('$destroy', function() {
227 angular.element($window)
228 .off('blur', windowBlurHandler)
229 .off('resize', debouncedOnResize);
230
231 parent
232 .off(ENTER_EVENTS, enterHandler)
233 .off(LEAVE_EVENTS, leaveHandler)
234 .off('mousedown', mousedownHandler);
235
236 // Trigger the handler in case any the tooltip was still visible.
237 leaveHandler();
238 document.removeEventListener('scroll', windowScrollHandler, true);
239 attributeObserver && attributeObserver.disconnect();
240 });
241
242 var enterHandler = function(e) {
243 // Prevent the tooltip from showing when the window is receiving focus.
244 if (e.type === 'focus' && elementFocusedOnWindowBlur) {
245 elementFocusedOnWindowBlur = false;
246 } else if (!scope.visible) {
247 parent.on(LEAVE_EVENTS, leaveHandler);
248 setVisible(true);
249
250 // If the user is on a touch device, we should bind the tap away after
251 // the `touched` in order to prevent the tooltip being removed immediately.
252 if (e.type === 'touchstart') {
253 parent.one('touchend', function() {
254 $mdUtil.nextTick(function() {
255 $document.one('touchend', leaveHandler);
256 }, false);
257 });
258 }
259 }
260 };
261 var leaveHandler = function () {
262 var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
263
264 if (autohide || mouseActive || $document[0].activeElement !== parent[0]) {
265 // When a show timeout is currently in progress, then we have to cancel it.
266 // Otherwise the tooltip will remain showing without focus or hover.
267 if (showTimeout) {
268 $timeout.cancel(showTimeout);
269 setVisible.queued = false;
270 showTimeout = null;
271 }
272
273 parent.off(LEAVE_EVENTS, leaveHandler);
274 parent.triggerHandler('blur');
275 setVisible(false);
276 }
277 mouseActive = false;
278 };
279 var mousedownHandler = function() {
280 mouseActive = true;
281 };
282
283 // to avoid `synthetic clicks` we listen to mousedown instead of `click`
284 parent.on('mousedown', mousedownHandler);
285 parent.on(ENTER_EVENTS, enterHandler);
286 }
287
288 function setVisible (value) {
289 // break if passed value is already in queue or there is no queue and passed value is current in the scope
290 if (setVisible.queued && setVisible.value === !!value || !setVisible.queued && scope.visible === !!value) return;
291 setVisible.value = !!value;
292
293 if (!setVisible.queued) {
294 if (value) {
295 setVisible.queued = true;
296 showTimeout = $timeout(function() {
297 scope.visible = setVisible.value;
298 setVisible.queued = false;
299 showTimeout = null;
300
301 if (!scope.visibleWatcher) {
302 onVisibleChanged(scope.visible);
303 }
304 }, scope.delay);
305 } else {
306 $mdUtil.nextTick(function() {
307 scope.visible = false;
308 if (!scope.visibleWatcher)
309 onVisibleChanged(false);
310 });
311 }
312 }
313 }
314
315 function showTooltip() {
316 // Do not show the tooltip if the text is empty.
317 if (!element[0].textContent.trim()) return;
318
319 // Insert the element and position at top left, so we can get the position
320 // and check if we should display it
321 element.css({top: 0, left: 0});
322 tooltipParent.append(element);
323
324 // Check if we should display it or not.
325 // This handles hide-* and show-* along with any user defined css
326 if ( $mdUtil.hasComputedStyle(element, 'display', 'none')) {
327 scope.visible = false;
328 element.detach();
329 return;
330 }
331
332 updatePosition();
333
334 $animate.addClass(content, SHOW_CLASS).then(function() {
335 element.addClass(SHOW_CLASS);
336 });
337 }
338
339 function hideTooltip() {
340 $animate.removeClass(content, SHOW_CLASS).then(function(){
341 element.removeClass(SHOW_CLASS);
342 if (!scope.visible) element.detach();
343 });
344 }
345
346 function updatePosition() {
347 if ( !scope.visible ) return;
348
349 updateContentOrigin();
350 positionTooltip();
351 }
352
353 function positionTooltip() {
354 var tipRect = $mdUtil.offsetRect(element, tooltipParent);
355 var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
356 var newPosition = getPosition(scope.direction);
357 var offsetParent = element.prop('offsetParent');
358
359 // If the user provided a direction, just nudge the tooltip onto the screen
360 // Otherwise, recalculate based on 'top' since default is 'bottom'
361 if (scope.direction) {
362 newPosition = fitInParent(newPosition);
363 } else if (offsetParent && newPosition.top > offsetParent.scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
364 newPosition = fitInParent(getPosition('top'));
365 }
366
367 element.css({
368 left: newPosition.left + 'px',
369 top: newPosition.top + 'px'
370 });
371
372 function fitInParent (pos) {
373 var newPosition = { left: pos.left, top: pos.top };
374 newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
375 newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
376 newPosition.top = Math.min( newPosition.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
377 newPosition.top = Math.max( newPosition.top, TOOLTIP_WINDOW_EDGE_SPACE );
378 return newPosition;
379 }
380
381 function getPosition (dir) {
382 return dir === 'left'
383 ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
384 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
385 : dir === 'right'
386 ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
387 top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
388 : dir === 'top'
389 ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
390 top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
391 : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
392 top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
393 }
394 }
395
396 }
397
398}
399
400})(window, window.angular);