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