socketioMEANseed/www/bower_components/angular-material/modules/closure/fabSpeedDial/fabSpeedDial.js
www/bower_components/angular-material/modules/closure/fabSpeedDial/fabSpeedDial.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
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 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
575ngmaterial.components.fabShared = angular.module("material.components.fabShared");