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