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);