socketioMEANseed/www/bower_components/angular-material/modules/closure/tooltip/tooltip.js
www/bower_components/angular-material/modules/closure/tooltip/tooltip.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.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 parent = $mdUtil.getParentWithPointerEvents(element);
89 var debouncedOnResize = $$rAF.throttle(updatePosition);
90 var mouseActive = false;
91 var origin, position, panelPosition, panelRef, autohide, showTimeout,
92 elementFocusedOnWindowBlur = null;
93
94 // Set defaults
95 setDefaults();
96
97 // Set parent aria-label.
98 addAriaLabel();
99
100 // Remove the element from its current DOM position.
101 element.detach();
102
103 updatePosition();
104 bindEvents();
105 configureWatchers();
106
107 function setDefaults() {
108 scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX;
109 scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY;
110 if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) {
111 scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION;
112 }
113 }
114
115 function addAriaLabel(override) {
116 if (override || !parent.attr('aria-label')) {
117 // Only interpolate the text from the HTML element because otherwise the custom text
118 // could be interpolated twice and cause XSS violations.
119 var interpolatedText = override || $interpolate(element.text().trim())(scope.$parent);
120 parent.attr('aria-label', interpolatedText);
121 }
122 }
123
124 function updatePosition() {
125 setDefaults();
126
127 // If the panel has already been created, remove the current origin
128 // class from the panel element.
129 if (panelRef && panelRef.panelEl) {
130 panelRef.panelEl.removeClass(origin);
131 }
132
133 // Set the panel element origin class based off of the current
134 // mdDirection.
135 origin = 'md-origin-' + scope.mdDirection;
136
137 // Create the position of the panel based off of the mdDirection.
138 position = TOOLTIP_DIRECTIONS[scope.mdDirection];
139
140 // Using the newly created position object, use the MdPanel
141 // panelPosition API to build the panel's position.
142 panelPosition = $mdPanel.newPanelPosition()
143 .relativeTo(parent)
144 .addPanelPosition(position.x, position.y);
145
146 // If the panel has already been created, add the new origin class to
147 // the panel element and update it's position with the panelPosition.
148 if (panelRef && panelRef.panelEl) {
149 panelRef.panelEl.addClass(origin);
150 panelRef.updatePosition(panelPosition);
151 }
152 }
153
154 function bindEvents() {
155 // Add a mutationObserver where there is support for it and the need
156 // for it in the form of viable host(parent[0]).
157 if (parent[0] && 'MutationObserver' in $window) {
158 // Use a mutationObserver to tackle #2602.
159 var attributeObserver = new MutationObserver(function(mutations) {
160 if (isDisabledMutation(mutations)) {
161 $mdUtil.nextTick(function() {
162 setVisible(false);
163 });
164 }
165 });
166
167 attributeObserver.observe(parent[0], {
168 attributes: true
169 });
170 }
171
172 elementFocusedOnWindowBlur = false;
173
174 $$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true);
175 $$mdTooltipRegistry.register('blur', windowBlurEventHandler);
176 $$mdTooltipRegistry.register('resize', debouncedOnResize);
177
178 scope.$on('$destroy', onDestroy);
179
180 // To avoid 'synthetic clicks', we listen to mousedown instead of
181 // 'click'.
182 parent.on('mousedown', mousedownEventHandler);
183 parent.on(ENTER_EVENTS, enterEventHandler);
184
185 function isDisabledMutation(mutations) {
186 mutations.some(function(mutation) {
187 return mutation.attributeName === 'disabled' && parent[0].disabled;
188 });
189 return false;
190 }
191
192 function windowScrollEventHandler() {
193 setVisible(false);
194 }
195
196 function windowBlurEventHandler() {
197 elementFocusedOnWindowBlur = document.activeElement === parent[0];
198 }
199
200 function enterEventHandler($event) {
201 // Prevent the tooltip from showing when the window is receiving
202 // focus.
203 if ($event.type === 'focus' && elementFocusedOnWindowBlur) {
204 elementFocusedOnWindowBlur = false;
205 } else if (!scope.mdVisible) {
206 parent.on(LEAVE_EVENTS, leaveEventHandler);
207 setVisible(true);
208
209 // If the user is on a touch device, we should bind the tap away
210 // after the 'touched' in order to prevent the tooltip being
211 // removed immediately.
212 if ($event.type === 'touchstart') {
213 parent.one('touchend', function() {
214 $mdUtil.nextTick(function() {
215 $document.one('touchend', leaveEventHandler);
216 }, false);
217 });
218 }
219 }
220 }
221
222 function leaveEventHandler() {
223 autohide = scope.hasOwnProperty('mdAutohide') ?
224 scope.mdAutohide :
225 attr.hasOwnProperty('mdAutohide');
226
227 if (autohide || mouseActive ||
228 $document[0].activeElement !== parent[0]) {
229 // When a show timeout is currently in progress, then we have
230 // to cancel it, otherwise the tooltip will remain showing
231 // without focus or hover.
232 if (showTimeout) {
233 $timeout.cancel(showTimeout);
234 setVisible.queued = false;
235 showTimeout = null;
236 }
237
238 parent.off(LEAVE_EVENTS, leaveEventHandler);
239 parent.triggerHandler('blur');
240 setVisible(false);
241 }
242 mouseActive = false;
243 }
244
245 function mousedownEventHandler() {
246 mouseActive = true;
247 }
248
249 function onDestroy() {
250 $$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true);
251 $$mdTooltipRegistry.deregister('blur', windowBlurEventHandler);
252 $$mdTooltipRegistry.deregister('resize', debouncedOnResize);
253
254 parent
255 .off(ENTER_EVENTS, enterEventHandler)
256 .off(LEAVE_EVENTS, leaveEventHandler)
257 .off('mousedown', mousedownEventHandler);
258
259 // Trigger the handler in case any of the tooltips are
260 // still visible.
261 leaveEventHandler();
262 attributeObserver && attributeObserver.disconnect();
263 }
264 }
265
266 function configureWatchers() {
267 if (element[0] && 'MutationObserver' in $window) {
268 var attributeObserver = new MutationObserver(function(mutations) {
269 mutations.forEach(function(mutation) {
270 if (mutation.attributeName === 'md-visible' &&
271 !scope.visibleWatcher ) {
272 scope.visibleWatcher = scope.$watch('mdVisible',
273 onVisibleChanged);
274 }
275 });
276 });
277
278 attributeObserver.observe(element[0], {
279 attributes: true
280 });
281
282 // Build watcher only if mdVisible is being used.
283 if (attr.hasOwnProperty('mdVisible')) {
284 scope.visibleWatcher = scope.$watch('mdVisible',
285 onVisibleChanged);
286 }
287 } else {
288 // MutationObserver not supported
289 scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged);
290 }
291
292 // Direction watcher
293 scope.$watch('mdDirection', updatePosition);
294
295 // Clean up if the element or parent was removed via jqLite's .remove.
296 // A couple of notes:
297 // - In these cases the scope might not have been destroyed, which
298 // is why we destroy it manually. An example of this can be having
299 // `md-visible="false"` and adding tooltips while they're
300 // invisible. If `md-visible` becomes true, at some point, you'd
301 // usually get a lot of tooltips.
302 // - We use `.one`, not `.on`, because this only needs to fire once.
303 // If we were using `.on`, it would get thrown into an infinite
304 // loop.
305 // - This kicks off the scope's `$destroy` event which finishes the
306 // cleanup.
307 element.one('$destroy', onElementDestroy);
308 parent.one('$destroy', onElementDestroy);
309 scope.$on('$destroy', function() {
310 setVisible(false);
311 panelRef && panelRef.destroy();
312 attributeObserver && attributeObserver.disconnect();
313 element.remove();
314 });
315
316 // Updates the aria-label when the element text changes. This watch
317 // doesn't need to be set up if the element doesn't have any data
318 // bindings.
319 if (element.text().indexOf($interpolate.startSymbol()) > -1) {
320 scope.$watch(function() {
321 return element.text().trim();
322 }, addAriaLabel);
323 }
324
325 function onElementDestroy() {
326 scope.$destroy();
327 }
328 }
329
330 function setVisible(value) {
331 // Break if passed value is already in queue or there is no queue and
332 // passed value is current in the controller.
333 if (setVisible.queued && setVisible.value === !!value ||
334 !setVisible.queued && scope.mdVisible === !!value) {
335 return;
336 }
337 setVisible.value = !!value;
338
339 if (!setVisible.queued) {
340 if (value) {
341 setVisible.queued = true;
342 showTimeout = $timeout(function() {
343 scope.mdVisible = setVisible.value;
344 setVisible.queued = false;
345 showTimeout = null;
346 if (!scope.visibleWatcher) {
347 onVisibleChanged(scope.mdVisible);
348 }
349 }, scope.mdDelay);
350 } else {
351 $mdUtil.nextTick(function() {
352 scope.mdVisible = false;
353 if (!scope.visibleWatcher) {
354 onVisibleChanged(false);
355 }
356 });
357 }
358 }
359 }
360
361 function onVisibleChanged(isVisible) {
362 isVisible ? showTooltip() : hideTooltip();
363 }
364
365 function showTooltip() {
366 // Do not show the tooltip if the text is empty.
367 if (!element[0].textContent.trim()) {
368 throw new Error('Text for the tooltip has not been provided. ' +
369 'Please include text within the mdTooltip element.');
370 }
371
372 if (!panelRef) {
373 var id = 'tooltip-' + $mdUtil.nextUid();
374 var attachTo = angular.element(document.body);
375 var panelAnimation = $mdPanel.newPanelAnimation()
376 .openFrom(parent)
377 .closeTo(parent)
378 .withAnimation({
379 open: 'md-show',
380 close: 'md-hide'
381 });
382
383 var panelConfig = {
384 id: id,
385 attachTo: attachTo,
386 contentElement: element,
387 propagateContainerEvents: true,
388 panelClass: 'md-tooltip ' + origin,
389 animation: panelAnimation,
390 position: panelPosition,
391 zIndex: scope.mdZIndex,
392 focusOnOpen: false
393 };
394
395 panelRef = $mdPanel.create(panelConfig);
396 }
397
398 panelRef.open().then(function() {
399 panelRef.panelEl.attr('role', 'tooltip');
400 });
401 }
402
403 function hideTooltip() {
404 panelRef && panelRef.close();
405 }
406 }
407
408}
409
410
411/**
412 * Service that is used to reduce the amount of listeners that are being
413 * registered on the `window` by the tooltip component. Works by collecting
414 * the individual event handlers and dispatching them from a global handler.
415 *
416 * ngInject
417 */
418function MdTooltipRegistry() {
419 var listeners = {};
420 var ngWindow = angular.element(window);
421
422 return {
423 register: register,
424 deregister: deregister
425 };
426
427 /**
428 * Global event handler that dispatches the registered handlers in the
429 * service.
430 * @param {!Event} event Event object passed in by the browser
431 */
432 function globalEventHandler(event) {
433 if (listeners[event.type]) {
434 listeners[event.type].forEach(function(currentHandler) {
435 currentHandler.call(this, event);
436 }, this);
437 }
438 }
439
440 /**
441 * Registers a new handler with the service.
442 * @param {string} type Type of event to be registered.
443 * @param {!Function} handler Event handler.
444 * @param {boolean} useCapture Whether to use event capturing.
445 */
446 function register(type, handler, useCapture) {
447 var handlers = listeners[type] = listeners[type] || [];
448
449 if (!handlers.length) {
450 useCapture ? window.addEventListener(type, globalEventHandler, true) :
451 ngWindow.on(type, globalEventHandler);
452 }
453
454 if (handlers.indexOf(handler) === -1) {
455 handlers.push(handler);
456 }
457 }
458
459 /**
460 * Removes an event handler from the service.
461 * @param {string} type Type of event handler.
462 * @param {!Function} handler The event handler itself.
463 * @param {boolean} useCapture Whether the event handler used event capturing.
464 */
465 function deregister(type, handler, useCapture) {
466 var handlers = listeners[type];
467 var index = handlers ? handlers.indexOf(handler) : -1;
468
469 if (index > -1) {
470 handlers.splice(index, 1);
471
472 if (handlers.length === 0) {
473 useCapture ? window.removeEventListener(type, globalEventHandler, true) :
474 ngWindow.off(type, globalEventHandler);
475 }
476 }
477 }
478}
479
480ngmaterial.components.tooltip = angular.module("material.components.tooltip");