arnaucube
arnaucube/commonroutesAppPublic Self-hosted repository
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);