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