source: trip-planner-front/node_modules/angular-material/modules/closure/tooltip/tooltip.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 16.2 KB
Line 
1/*!
2 * AngularJS Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.2.3
6 */
7goog.provide('ngmaterial.components.tooltip');
8goog.require('ngmaterial.components.panel');
9goog.require('ngmaterial.core');
10/**
11 * @ngdoc module
12 * @name material.components.tooltip
13 */
14MdTooltipDirective['$inject'] = ["$timeout", "$window", "$$rAF", "$document", "$interpolate", "$mdUtil", "$mdPanel", "$$mdTooltipRegistry"];
15angular
16 .module('material.components.tooltip', [
17 'material.core',
18 'material.components.panel'
19 ])
20 .directive('mdTooltip', MdTooltipDirective)
21 .service('$$mdTooltipRegistry', MdTooltipRegistry);
22
23
24/**
25 * @ngdoc directive
26 * @name mdTooltip
27 * @module material.components.tooltip
28 * @description
29 * Tooltips are used to describe elements that are interactive and primarily
30 * graphical (not textual).
31 *
32 * Place a `<md-tooltip>` as a child of the element it describes.
33 *
34 * A tooltip will activate when the user hovers over, focuses, or touches the
35 * parent element.
36 *
37 * @usage
38 * <hljs lang="html">
39 * <md-button class="md-fab md-accent" aria-label="Play">
40 * <md-tooltip>Play Music</md-tooltip>
41 * <md-icon md-svg-src="img/icons/ic_play_arrow_24px.svg"></md-icon>
42 * </md-button>
43 * </hljs>
44 *
45 * @param {number=} md-z-index The visual level that the tooltip will appear
46 * in comparison with the rest of the elements of the application.
47 * @param {expression=} md-visible Boolean bound to whether the tooltip is
48 * currently visible.
49 * @param {number=} md-delay How many milliseconds to wait to show the tooltip
50 * after the user hovers over, focuses, or touches the parent element.
51 * Defaults to 0ms on non-touch devices and 75ms on touch.
52 * @param {boolean=} md-autohide If present or provided with a boolean value,
53 * the tooltip will hide on mouse leave, regardless of focus.
54 * @param {string=} md-direction The direction that the tooltip is shown,
55 * relative to the parent element. Supports top, right, bottom, and left.
56 * Defaults to bottom.
57 */
58function MdTooltipDirective($timeout, $window, $$rAF, $document, $interpolate,
59 $mdUtil, $mdPanel, $$mdTooltipRegistry) {
60
61 var ENTER_EVENTS = 'focus touchstart mouseenter';
62 var LEAVE_EVENTS = 'blur touchcancel mouseleave';
63 var TOOLTIP_DEFAULT_Z_INDEX = 100;
64 var TOOLTIP_DEFAULT_SHOW_DELAY = 0;
65 var TOOLTIP_DEFAULT_DIRECTION = 'bottom';
66 var TOOLTIP_DIRECTIONS = {
67 top: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.ABOVE },
68 right: { x: $mdPanel.xPosition.OFFSET_END, y: $mdPanel.yPosition.CENTER },
69 bottom: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.BELOW },
70 left: { x: $mdPanel.xPosition.OFFSET_START, y: $mdPanel.yPosition.CENTER }
71 };
72
73 return {
74 restrict: 'E',
75 priority: 210, // Before ngAria
76 scope: {
77 mdZIndex: '=?mdZIndex',
78 mdDelay: '=?mdDelay',
79 mdVisible: '=?mdVisible',
80 mdAutohide: '=?mdAutohide',
81 mdDirection: '@?mdDirection' // Do not expect expressions.
82 },
83 link: linkFunc
84 };
85
86 function linkFunc(scope, element, attr) {
87 // Set constants.
88 var tooltipId = 'md-tooltip-' + $mdUtil.nextUid();
89 var parent = $mdUtil.getParentWithPointerEvents(element);
90 var debouncedOnResize = $$rAF.throttle(updatePosition);
91 var mouseActive = false;
92 var origin, position, panelPosition, panelRef, autohide, showTimeout,
93 elementFocusedOnWindowBlur = null;
94
95 // Set defaults
96 setDefaults();
97
98 // Set parent aria-label.
99 addAriaLabel();
100
101 // Remove the element from its current DOM position.
102 element.detach();
103
104 updatePosition();
105 bindEvents();
106 configureWatchers();
107
108 function setDefaults() {
109 scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX;
110 scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY;
111 if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) {
112 scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION;
113 }
114 }
115
116 function addAriaLabel(labelText) {
117 // Only interpolate the text from the HTML element because otherwise the custom text could
118 // be interpolated twice and cause XSS violations.
119 var interpolatedText = labelText || $interpolate(element.text().trim())(scope.$parent);
120
121 // Only add the `aria-label` to the parent if there isn't already one, if there isn't an
122 // already present `aria-labelledby`, or if the previous `aria-label` was added by the
123 // tooltip directive.
124 if (
125 (!parent.attr('aria-label') && !parent.attr('aria-labelledby')) ||
126 parent.attr('md-labeled-by-tooltip')
127 ) {
128 parent.attr('aria-label', interpolatedText);
129
130 // Set the `md-labeled-by-tooltip` attribute if it has not already been set.
131 if (!parent.attr('md-labeled-by-tooltip')) {
132 parent.attr('md-labeled-by-tooltip', tooltipId);
133 }
134 }
135 }
136
137 function updatePosition() {
138 setDefaults();
139
140 // If the panel has already been created, remove the current origin
141 // class from the panel element.
142 if (panelRef && panelRef.panelEl) {
143 panelRef.panelEl.removeClass(origin);
144 }
145
146 // Set the panel element origin class based off of the current
147 // mdDirection.
148 origin = 'md-origin-' + scope.mdDirection;
149
150 // Create the position of the panel based off of the mdDirection.
151 position = TOOLTIP_DIRECTIONS[scope.mdDirection];
152
153 // Using the newly created position object, use the MdPanel
154 // panelPosition API to build the panel's position.
155 panelPosition = $mdPanel.newPanelPosition()
156 .relativeTo(parent)
157 .addPanelPosition(position.x, position.y);
158
159 // If the panel has already been created, add the new origin class to
160 // the panel element and update it's position with the panelPosition.
161 if (panelRef && panelRef.panelEl) {
162 panelRef.panelEl.addClass(origin);
163 panelRef.updatePosition(panelPosition);
164 }
165 }
166
167 function bindEvents() {
168 // Add a mutationObserver where there is support for it and the need
169 // for it in the form of viable host(parent[0]).
170 if (parent[0] && 'MutationObserver' in $window) {
171 // Use a mutationObserver to tackle #2602.
172 var attributeObserver = new MutationObserver(function(mutations) {
173 if (isDisabledMutation(mutations)) {
174 $mdUtil.nextTick(function() {
175 setVisible(false);
176 });
177 }
178 });
179
180 attributeObserver.observe(parent[0], {
181 attributes: true
182 });
183 }
184
185 elementFocusedOnWindowBlur = false;
186
187 $$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true);
188 $$mdTooltipRegistry.register('blur', windowBlurEventHandler);
189 $$mdTooltipRegistry.register('resize', debouncedOnResize);
190
191 scope.$on('$destroy', onDestroy);
192
193 // To avoid 'synthetic clicks', we listen to mousedown instead of
194 // 'click'.
195 parent.on('mousedown', mousedownEventHandler);
196 parent.on(ENTER_EVENTS, enterEventHandler);
197
198 function isDisabledMutation(mutations) {
199 mutations.some(function(mutation) {
200 return mutation.attributeName === 'disabled' && parent[0].disabled;
201 });
202 return false;
203 }
204
205 function windowScrollEventHandler() {
206 setVisible(false);
207 }
208
209 function windowBlurEventHandler() {
210 elementFocusedOnWindowBlur = document.activeElement === parent[0];
211 }
212
213 function enterEventHandler($event) {
214 // Prevent the tooltip from showing when the window is receiving
215 // focus.
216 if ($event.type === 'focus' && elementFocusedOnWindowBlur) {
217 elementFocusedOnWindowBlur = false;
218 } else if (!scope.mdVisible) {
219 parent.on(LEAVE_EVENTS, leaveEventHandler);
220 setVisible(true);
221
222 // If the user is on a touch device, we should bind the tap away
223 // after the 'touched' in order to prevent the tooltip being
224 // removed immediately.
225 if ($event.type === 'touchstart') {
226 parent.one('touchend', function() {
227 $mdUtil.nextTick(function() {
228 $document.one('touchend', leaveEventHandler);
229 }, false);
230 });
231 }
232 }
233 }
234
235 function leaveEventHandler() {
236 autohide = scope.hasOwnProperty('mdAutohide') ?
237 scope.mdAutohide :
238 attr.hasOwnProperty('mdAutohide');
239
240 if (autohide || mouseActive ||
241 $document[0].activeElement !== parent[0]) {
242 // When a show timeout is currently in progress, then we have
243 // to cancel it, otherwise the tooltip will remain showing
244 // without focus or hover.
245 if (showTimeout) {
246 $timeout.cancel(showTimeout);
247 setVisible.queued = false;
248 showTimeout = null;
249 }
250
251 parent.off(LEAVE_EVENTS, leaveEventHandler);
252 parent.triggerHandler('blur');
253 setVisible(false);
254 }
255 mouseActive = false;
256 }
257
258 function mousedownEventHandler() {
259 mouseActive = true;
260 }
261
262 function onDestroy() {
263 $$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true);
264 $$mdTooltipRegistry.deregister('blur', windowBlurEventHandler);
265 $$mdTooltipRegistry.deregister('resize', debouncedOnResize);
266
267 parent
268 .off(ENTER_EVENTS, enterEventHandler)
269 .off(LEAVE_EVENTS, leaveEventHandler)
270 .off('mousedown', mousedownEventHandler);
271
272 // Trigger the handler in case any of the tooltips are
273 // still visible.
274 leaveEventHandler();
275 attributeObserver && attributeObserver.disconnect();
276 }
277 }
278
279 function configureWatchers() {
280 if (element[0] && 'MutationObserver' in $window) {
281 var attributeObserver = new MutationObserver(function(mutations) {
282 mutations.forEach(function(mutation) {
283 if (mutation.attributeName === 'md-visible' &&
284 !scope.visibleWatcher) {
285 scope.visibleWatcher = scope.$watch('mdVisible',
286 onVisibleChanged);
287 }
288 });
289 });
290
291 attributeObserver.observe(element[0], {
292 attributes: true
293 });
294
295 // Build watcher only if mdVisible is being used.
296 if (attr.hasOwnProperty('mdVisible')) {
297 scope.visibleWatcher = scope.$watch('mdVisible',
298 onVisibleChanged);
299 }
300 } else {
301 // MutationObserver not supported
302 scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged);
303 }
304
305 // Direction watcher
306 scope.$watch('mdDirection', updatePosition);
307
308 // Clean up if the element or parent was removed via jqLite's .remove.
309 // A couple of notes:
310 // - In these cases the scope might not have been destroyed, which
311 // is why we destroy it manually. An example of this can be having
312 // `md-visible="false"` and adding tooltips while they're
313 // invisible. If `md-visible` becomes true, at some point, you'd
314 // usually get a lot of tooltips.
315 // - We use `.one`, not `.on`, because this only needs to fire once.
316 // If we were using `.on`, it would get thrown into an infinite
317 // loop.
318 // - This kicks off the scope's `$destroy` event which finishes the
319 // cleanup.
320 element.one('$destroy', onElementDestroy);
321 parent.one('$destroy', onElementDestroy);
322 scope.$on('$destroy', function() {
323 setVisible(false);
324 panelRef && panelRef.destroy();
325 attributeObserver && attributeObserver.disconnect();
326 element.remove();
327 });
328
329 // Updates the aria-label when the element text changes. This watch
330 // doesn't need to be set up if the element doesn't have any data
331 // bindings.
332 if (element.text().indexOf($interpolate.startSymbol()) > -1) {
333 scope.$watch(function() {
334 return element.text().trim();
335 }, addAriaLabel);
336 }
337
338 function onElementDestroy() {
339 scope.$destroy();
340 }
341 }
342
343 function setVisible(value) {
344 // Break if passed value is already in queue or there is no queue and
345 // passed value is current in the controller.
346 if (setVisible.queued && setVisible.value === !!value ||
347 !setVisible.queued && scope.mdVisible === !!value) {
348 return;
349 }
350 setVisible.value = !!value;
351
352 if (!setVisible.queued) {
353 if (value) {
354 setVisible.queued = true;
355 showTimeout = $timeout(function() {
356 scope.mdVisible = setVisible.value;
357 setVisible.queued = false;
358 showTimeout = null;
359 if (!scope.visibleWatcher) {
360 onVisibleChanged(scope.mdVisible);
361 }
362 }, scope.mdDelay);
363 } else {
364 $mdUtil.nextTick(function() {
365 scope.mdVisible = false;
366 if (!scope.visibleWatcher) {
367 onVisibleChanged(false);
368 }
369 });
370 }
371 }
372 }
373
374 function onVisibleChanged(isVisible) {
375 isVisible ? showTooltip() : hideTooltip();
376 }
377
378 function showTooltip() {
379 // Do not show the tooltip if the text is empty.
380 if (!element[0].textContent.trim()) {
381 throw new Error('Text for the tooltip has not been provided. ' +
382 'Please include text within the mdTooltip element.');
383 }
384
385 if (!panelRef) {
386 var attachTo = angular.element(document.body);
387 var panelAnimation = $mdPanel.newPanelAnimation()
388 .openFrom(parent)
389 .closeTo(parent)
390 .withAnimation({
391 open: 'md-show',
392 close: 'md-hide'
393 });
394
395 var panelConfig = {
396 id: tooltipId,
397 attachTo: attachTo,
398 contentElement: element,
399 propagateContainerEvents: true,
400 panelClass: 'md-tooltip',
401 animation: panelAnimation,
402 position: panelPosition,
403 zIndex: scope.mdZIndex,
404 focusOnOpen: false,
405 onDomAdded: function() {
406 panelRef.panelEl.addClass(origin);
407 }
408 };
409
410 panelRef = $mdPanel.create(panelConfig);
411 }
412
413 panelRef.open().then(function() {
414 panelRef.panelEl.attr('role', 'tooltip');
415 });
416 }
417
418 function hideTooltip() {
419 panelRef && panelRef.close();
420 }
421 }
422
423}
424
425
426/**
427 * Service that is used to reduce the amount of listeners that are being
428 * registered on the `window` by the tooltip component. Works by collecting
429 * the individual event handlers and dispatching them from a global handler.
430 *
431 * ngInject
432 */
433function MdTooltipRegistry() {
434 var listeners = {};
435 var ngWindow = angular.element(window);
436
437 return {
438 register: register,
439 deregister: deregister
440 };
441
442 /**
443 * Global event handler that dispatches the registered handlers in the
444 * service.
445 * @param {!Event} event Event object passed in by the browser
446 */
447 function globalEventHandler(event) {
448 if (listeners[event.type]) {
449 listeners[event.type].forEach(function(currentHandler) {
450 currentHandler.call(this, event);
451 }, this);
452 }
453 }
454
455 /**
456 * Registers a new handler with the service.
457 * @param {string} type Type of event to be registered.
458 * @param {!Function} handler Event handler.
459 * @param {boolean} useCapture Whether to use event capturing.
460 */
461 function register(type, handler, useCapture) {
462 var handlers = listeners[type] = listeners[type] || [];
463
464 if (!handlers.length) {
465 useCapture ? window.addEventListener(type, globalEventHandler, true) :
466 ngWindow.on(type, globalEventHandler);
467 }
468
469 if (handlers.indexOf(handler) === -1) {
470 handlers.push(handler);
471 }
472 }
473
474 /**
475 * Removes an event handler from the service.
476 * @param {string} type Type of event handler.
477 * @param {!Function} handler The event handler itself.
478 * @param {boolean} useCapture Whether the event handler used event capturing.
479 */
480 function deregister(type, handler, useCapture) {
481 var handlers = listeners[type];
482 var index = handlers ? handlers.indexOf(handler) : -1;
483
484 if (index > -1) {
485 handlers.splice(index, 1);
486
487 if (handlers.length === 0) {
488 useCapture ? window.removeEventListener(type, globalEventHandler, true) :
489 ngWindow.off(type, globalEventHandler);
490 }
491 }
492 }
493}
494
495ngmaterial.components.tooltip = angular.module("material.components.tooltip");
Note: See TracBrowser for help on using the repository browser.