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