arnaucube
arnaucube/commonroutesAppPublic Self-hosted repository
commonroutesApp/www/lib/angular-material/modules/js/fabSpeedDial/fabSpeedDial.js
www/lib/angular-material/modules/js/fabSpeedDial/fabSpeedDial.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(function() {
 11  'use strict';
 12
 13  MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
 14  angular.module('material.components.fabShared', ['material.core'])
 15    .controller('MdFabController', MdFabController);
 16
 17  function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
 18    var vm = this;
 19
 20    // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
 21
 22    vm.open = function() {
 23      $scope.$evalAsync("vm.isOpen = true");
 24    };
 25
 26    vm.close = function() {
 27      // Async eval to avoid conflicts with existing digest loops
 28      $scope.$evalAsync("vm.isOpen = false");
 29
 30      // Focus the trigger when the element closes so users can still tab to the next item
 31      $element.find('md-fab-trigger')[0].focus();
 32    };
 33
 34    // Toggle the open/close state when the trigger is clicked
 35    vm.toggle = function() {
 36      $scope.$evalAsync("vm.isOpen = !vm.isOpen");
 37    };
 38
 39    setupDefaults();
 40    setupListeners();
 41    setupWatchers();
 42
 43    var initialAnimationAttempts = 0;
 44    fireInitialAnimations();
 45
 46    function setupDefaults() {
 47      // Set the default direction to 'down' if none is specified
 48      vm.direction = vm.direction || 'down';
 49
 50      // Set the default to be closed
 51      vm.isOpen = vm.isOpen || false;
 52
 53      // Start the keyboard interaction at the first action
 54      resetActionIndex();
 55
 56      // Add an animations waiting class so we know not to run
 57      $element.addClass('md-animations-waiting');
 58    }
 59
 60    function setupListeners() {
 61      var eventTypes = [
 62        'click', 'focusin', 'focusout'
 63      ];
 64
 65      // Add our listeners
 66      angular.forEach(eventTypes, function(eventType) {
 67        $element.on(eventType, parseEvents);
 68      });
 69
 70      // Remove our listeners when destroyed
 71      $scope.$on('$destroy', function() {
 72        angular.forEach(eventTypes, function(eventType) {
 73          $element.off(eventType, parseEvents);
 74        });
 75
 76        // remove any attached keyboard handlers in case element is removed while
 77        // speed dial is open
 78        disableKeyboard();
 79      });
 80    }
 81
 82    var closeTimeout;
 83    function parseEvents(event) {
 84      // If the event is a click, just handle it
 85      if (event.type == 'click') {
 86        handleItemClick(event);
 87      }
 88
 89      // If we focusout, set a timeout to close the element
 90      if (event.type == 'focusout' && !closeTimeout) {
 91        closeTimeout = $timeout(function() {
 92          vm.close();
 93        }, 100, false);
 94      }
 95
 96      // If we see a focusin and there is a timeout about to run, cancel it so we stay open
 97      if (event.type == 'focusin' && closeTimeout) {
 98        $timeout.cancel(closeTimeout);
 99        closeTimeout = null;
100      }
101    }
102
103    function resetActionIndex() {
104      vm.currentActionIndex = -1;
105    }
106
107    function setupWatchers() {
108      // Watch for changes to the direction and update classes/attributes
109      $scope.$watch('vm.direction', function(newDir, oldDir) {
110        // Add the appropriate classes so we can target the direction in the CSS
111        $animate.removeClass($element, 'md-' + oldDir);
112        $animate.addClass($element, 'md-' + newDir);
113
114        // Reset the action index since it may have changed
115        resetActionIndex();
116      });
117
118      var trigger, actions;
119
120      // Watch for changes to md-open
121      $scope.$watch('vm.isOpen', function(isOpen) {
122        // Reset the action index since it may have changed
123        resetActionIndex();
124
125        // We can't get the trigger/actions outside of the watch because the component hasn't been
126        // linked yet, so we wait until the first watch fires to cache them.
127        if (!trigger || !actions) {
128          trigger = getTriggerElement();
129          actions = getActionsElement();
130        }
131
132        if (isOpen) {
133          enableKeyboard();
134        } else {
135          disableKeyboard();
136        }
137
138        var toAdd = isOpen ? 'md-is-open' : '';
139        var toRemove = isOpen ? '' : 'md-is-open';
140
141        // Set the proper ARIA attributes
142        trigger.attr('aria-haspopup', true);
143        trigger.attr('aria-expanded', isOpen);
144        actions.attr('aria-hidden', !isOpen);
145
146        // Animate the CSS classes
147        $animate.setClass($element, toAdd, toRemove);
148      });
149    }
150
151    function fireInitialAnimations() {
152      // If the element is actually visible on the screen
153      if ($element[0].scrollHeight > 0) {
154        // Fire our animation
155        $animate.addClass($element, '_md-animations-ready').then(function() {
156          // Remove the waiting class
157          $element.removeClass('md-animations-waiting');
158        });
159      }
160
161      // Otherwise, try for up to 1 second before giving up
162      else if (initialAnimationAttempts < 10) {
163        $timeout(fireInitialAnimations, 100);
164
165        // Increment our counter
166        initialAnimationAttempts = initialAnimationAttempts + 1;
167      }
168    }
169
170    function enableKeyboard() {
171      $element.on('keydown', keyPressed);
172
173      // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid
174      // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button)
175      $mdUtil.nextTick(function() {
176        angular.element(document).on('click touchend', checkForOutsideClick);
177      });
178
179      // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but
180      // this breaks accessibility, especially on mobile, since you have no arrow keys to press
181      //resetActionTabIndexes();
182    }
183
184    function disableKeyboard() {
185      $element.off('keydown', keyPressed);
186      angular.element(document).off('click touchend', checkForOutsideClick);
187    }
188
189    function checkForOutsideClick(event) {
190      if (event.target) {
191        var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
192        var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
193
194        if (!closestTrigger && !closestActions) {
195          vm.close();
196        }
197      }
198    }
199
200    function keyPressed(event) {
201      switch (event.which) {
202        case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
203        case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
204        case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
205        case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
206        case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
207      }
208    }
209
210    function doActionPrev(event) {
211      focusAction(event, -1);
212    }
213
214    function doActionNext(event) {
215      focusAction(event, 1);
216    }
217
218    function focusAction(event, direction) {
219      var actions = resetActionTabIndexes();
220
221      // Increment/decrement the counter with restrictions
222      vm.currentActionIndex = vm.currentActionIndex + direction;
223      vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
224      vm.currentActionIndex = Math.max(0, vm.currentActionIndex);
225
226      // Focus the element
227      var focusElement =  angular.element(actions[vm.currentActionIndex]).children()[0];
228      angular.element(focusElement).attr('tabindex', 0);
229      focusElement.focus();
230
231      // Make sure the event doesn't bubble and cause something else
232      event.preventDefault();
233      event.stopImmediatePropagation();
234    }
235
236    function resetActionTabIndexes() {
237      // Grab all of the actions
238      var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
239
240      // Disable all other actions for tabbing
241      angular.forEach(actions, function(action) {
242        angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
243      });
244
245      return actions;
246    }
247
248    function doKeyLeft(event) {
249      if (vm.direction === 'left') {
250        doActionNext(event);
251      } else {
252        doActionPrev(event);
253      }
254    }
255
256    function doKeyUp(event) {
257      if (vm.direction === 'down') {
258        doActionPrev(event);
259      } else {
260        doActionNext(event);
261      }
262    }
263
264    function doKeyRight(event) {
265      if (vm.direction === 'left') {
266        doActionPrev(event);
267      } else {
268        doActionNext(event);
269      }
270    }
271
272    function doKeyDown(event) {
273      if (vm.direction === 'up') {
274        doActionPrev(event);
275      } else {
276        doActionNext(event);
277      }
278    }
279
280    function isTrigger(element) {
281      return $mdUtil.getClosest(element, 'md-fab-trigger');
282    }
283
284    function isAction(element) {
285      return $mdUtil.getClosest(element, 'md-fab-actions');
286    }
287
288    function handleItemClick(event) {
289      if (isTrigger(event.target)) {
290        vm.toggle();
291      }
292
293      if (isAction(event.target)) {
294        vm.close();
295      }
296    }
297
298    function getTriggerElement() {
299      return $element.find('md-fab-trigger');
300    }
301
302    function getActionsElement() {
303      return $element.find('md-fab-actions');
304    }
305  }
306})();
307
308(function() {
309  'use strict';
310
311  /**
312   * The duration of the CSS animation in milliseconds.
313   *
314   * @type {number}
315   */
316  MdFabSpeedDialFlingAnimation.$inject = ["$timeout"];
317  MdFabSpeedDialScaleAnimation.$inject = ["$timeout"];
318  var cssAnimationDuration = 300;
319
320  /**
321   * @ngdoc module
322   * @name material.components.fabSpeedDial
323   */
324  angular
325    // Declare our module
326    .module('material.components.fabSpeedDial', [
327      'material.core',
328      'material.components.fabShared',
329      'material.components.fabActions'
330    ])
331
332    // Register our directive
333    .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
334
335    // Register our custom animations
336    .animation('.md-fling', MdFabSpeedDialFlingAnimation)
337    .animation('.md-scale', MdFabSpeedDialScaleAnimation)
338
339    // Register a service for each animation so that we can easily inject them into unit tests
340    .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation)
341    .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation);
342
343  /**
344   * @ngdoc directive
345   * @name mdFabSpeedDial
346   * @module material.components.fabSpeedDial
347   *
348   * @restrict E
349   *
350   * @description
351   * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
352   * `<md-button>`s) for quick access to common actions.
353   *
354   * There are currently two animations available by applying one of the following classes to
355   * the component:
356   *
357   *  - `md-fling` - The speed dial items appear from underneath the trigger and move into their
358   *    appropriate positions.
359   *  - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
360   *
361   * You may also easily position the trigger by applying one one of the following classes to the
362   * `<md-fab-speed-dial>` element:
363   *  - `md-fab-top-left`
364   *  - `md-fab-top-right`
365   *  - `md-fab-bottom-left`
366   *  - `md-fab-bottom-right`
367   *
368   * These CSS classes use `position: absolute`, so you need to ensure that the container element
369   * also uses `position: absolute` or `position: relative` in order for them to work.
370   *
371   * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to
372   * open or close the speed dial. However, if you wish to allow users to hover over the empty
373   * space where the actions will appear, you must also add the `md-hover-full` class to the speed
374   * dial element. Without this, the hover effect will only occur on top of the trigger.
375   *
376   * See the demos for more information.
377   *
378   * ## Troubleshooting
379   *
380   * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on
381   * the parent container to ensure that it is only visible once ready. We have plans to remove this
382   * necessity in the future.
383   *
384   * @usage
385   * <hljs lang="html">
386   * <md-fab-speed-dial md-direction="up" class="md-fling">
387   *   <md-fab-trigger>
388   *     <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
389   *   </md-fab-trigger>
390   *
391   *   <md-fab-actions>
392   *     <md-button aria-label="Add User">
393   *       <md-icon icon="/img/icons/user.svg"></md-icon>
394   *     </md-button>
395   *
396   *     <md-button aria-label="Add Group">
397   *       <md-icon icon="/img/icons/group.svg"></md-icon>
398   *     </md-button>
399   *   </md-fab-actions>
400   * </md-fab-speed-dial>
401   * </hljs>
402   *
403   * @param {string} md-direction From which direction you would like the speed dial to appear
404   * relative to the trigger element.
405   * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
406   */
407  function MdFabSpeedDialDirective() {
408    return {
409      restrict: 'E',
410
411      scope: {
412        direction: '@?mdDirection',
413        isOpen: '=?mdOpen'
414      },
415
416      bindToController: true,
417      controller: 'MdFabController',
418      controllerAs: 'vm',
419
420      link: FabSpeedDialLink
421    };
422
423    function FabSpeedDialLink(scope, element) {
424      // Prepend an element to hold our CSS variables so we can use them in the animations below
425      element.prepend('<div class="_md-css-variables"></div>');
426    }
427  }
428
429  function MdFabSpeedDialFlingAnimation($timeout) {
430    function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
431
432    function runAnimation(element) {
433      // Don't run if we are still waiting and we are not ready
434      if (element.hasClass('md-animations-waiting') && !element.hasClass('_md-animations-ready')) {
435        return;
436      }
437
438      var el = element[0];
439      var ctrl = element.controller('mdFabSpeedDial');
440      var items = el.querySelectorAll('.md-fab-action-item');
441
442      // Grab our trigger element
443      var triggerElement = el.querySelector('md-fab-trigger');
444
445      // Grab our element which stores CSS variables
446      var variablesElement = el.querySelector('._md-css-variables');
447
448      // Setup JS variables based on our CSS variables
449      var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
450
451      // Always reset the items to their natural position/state
452      angular.forEach(items, function(item, index) {
453        var styles = item.style;
454
455        styles.transform = styles.webkitTransform = '';
456        styles.transitionDelay = '';
457        styles.opacity = 1;
458
459        // Make the items closest to the trigger have the highest z-index
460        styles.zIndex = (items.length - index) + startZIndex;
461      });
462
463      // Set the trigger to be above all of the actions so they disappear behind it.
464      triggerElement.style.zIndex = startZIndex + items.length + 1;
465
466      // If the control is closed, hide the items behind the trigger
467      if (!ctrl.isOpen) {
468        angular.forEach(items, function(item, index) {
469          var newPosition, axis;
470          var styles = item.style;
471
472          // Make sure to account for differences in the dimensions of the trigger verses the items
473          // so that we can properly center everything; this helps hide the item's shadows behind
474          // the trigger.
475          var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
476          var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
477
478          switch (ctrl.direction) {
479            case 'up':
480              newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
481              axis = 'Y';
482              break;
483            case 'down':
484              newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
485              axis = 'Y';
486              break;
487            case 'left':
488              newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
489              axis = 'X';
490              break;
491            case 'right':
492              newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
493              axis = 'X';
494              break;
495          }
496
497          var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
498
499          styles.transform = styles.webkitTransform = newTranslate;
500        });
501      }
502    }
503
504    return {
505      addClass: function(element, className, done) {
506        if (element.hasClass('md-fling')) {
507          runAnimation(element);
508          delayDone(done);
509        } else {
510          done();
511        }
512      },
513      removeClass: function(element, className, done) {
514        runAnimation(element);
515        delayDone(done);
516      }
517    }
518  }
519
520  function MdFabSpeedDialScaleAnimation($timeout) {
521    function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
522
523    var delay = 65;
524
525    function runAnimation(element) {
526      var el = element[0];
527      var ctrl = element.controller('mdFabSpeedDial');
528      var items = el.querySelectorAll('.md-fab-action-item');
529
530      // Grab our element which stores CSS variables
531      var variablesElement = el.querySelector('._md-css-variables');
532
533      // Setup JS variables based on our CSS variables
534      var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
535
536      // Always reset the items to their natural position/state
537      angular.forEach(items, function(item, index) {
538        var styles = item.style,
539          offsetDelay = index * delay;
540
541        styles.opacity = ctrl.isOpen ? 1 : 0;
542        styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
543        styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
544
545        // Make the items closest to the trigger have the highest z-index
546        styles.zIndex = (items.length - index) + startZIndex;
547      });
548    }
549
550    return {
551      addClass: function(element, className, done) {
552        runAnimation(element);
553        delayDone(done);
554      },
555
556      removeClass: function(element, className, done) {
557        runAnimation(element);
558        delayDone(done);
559      }
560    }
561  }
562})();
563
564})(window, window.angular);