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