arnaucube
arnaucube/socketioMEANseedPublic Self-hosted repository
socketioMEANseed/www/bower_components/angular-material/modules/js/tooltip/tooltip.js
www/bower_components/angular-material/modules/js/tooltip/tooltip.jsRaw
  1/*!
  2 * Angular Material Design
  3 * https://github.com/angular/material
  4 * @license MIT
  5 * v1.1.3
  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", "$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
480})(window, window.angular);