2  * Angular Material Design
 
   3  * https://github.com/angular/material
 
   7 goog.provide('ngmaterial.components.fabShared');
 
   8 goog.require('ngmaterial.core');
 
  12   MdFabController['$inject'] = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
 
  13   angular.module('material.components.fabShared', ['material.core'])
 
  14     .controller('MdFabController', MdFabController);
 
  16   function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
 
  18     var initialAnimationAttempts = 0;
 
  20     // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
 
  22     vm.open = function() {
 
  23       $scope.$evalAsync("vm.isOpen = true");
 
  26     vm.close = function() {
 
  27       // Async eval to avoid conflicts with existing digest loops
 
  28       $scope.$evalAsync("vm.isOpen = false");
 
  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();
 
  34     // Toggle the open/close state when the trigger is clicked
 
  35     vm.toggle = function() {
 
  36       $scope.$evalAsync("vm.isOpen = !vm.isOpen");
 
  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.
 
  43     vm.$onInit = function() {
 
  48       fireInitialAnimations();
 
  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) {
 
  57     function setupDefaults() {
 
  58       // Set the default direction to 'down' if none is specified
 
  59       vm.direction = vm.direction || 'down';
 
  61       // Set the default to be closed
 
  62       vm.isOpen = vm.isOpen || false;
 
  64       // Start the keyboard interaction at the first action
 
  67       // Add an animations waiting class so we know not to run
 
  68       $element.addClass('md-animations-waiting');
 
  71     function setupListeners() {
 
  73         'click', 'focusin', 'focusout'
 
  77       angular.forEach(eventTypes, function(eventType) {
 
  78         $element.on(eventType, parseEvents);
 
  81       // Remove our listeners when destroyed
 
  82       $scope.$on('$destroy', function() {
 
  83         angular.forEach(eventTypes, function(eventType) {
 
  84           $element.off(eventType, parseEvents);
 
  87         // remove any attached keyboard handlers in case element is removed while
 
  94     function parseEvents(event) {
 
  95       // If the event is a click, just handle it
 
  96       if (event.type == 'click') {
 
  97         handleItemClick(event);
 
 100       // If we focusout, set a timeout to close the element
 
 101       if (event.type == 'focusout' && !closeTimeout) {
 
 102         closeTimeout = $timeout(function() {
 
 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);
 
 114     function resetActionIndex() {
 
 115       vm.currentActionIndex = -1;
 
 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);
 
 125         // Reset the action index since it may have changed
 
 129       var trigger, actions;
 
 131       // Watch for changes to md-open
 
 132       $scope.$watch('vm.isOpen', function(isOpen) {
 
 133         // Reset the action index since it may have changed
 
 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();
 
 149         var toAdd = isOpen ? 'md-is-open' : '';
 
 150         var toRemove = isOpen ? '' : 'md-is-open';
 
 152         // Set the proper ARIA attributes
 
 153         trigger.attr('aria-haspopup', true);
 
 154         trigger.attr('aria-expanded', isOpen);
 
 155         actions.attr('aria-hidden', !isOpen);
 
 157         // Animate the CSS classes
 
 158         $animate.setClass($element, toAdd, toRemove);
 
 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');
 
 172       // Otherwise, try for up to 1 second before giving up
 
 173       else if (initialAnimationAttempts < 10) {
 
 174         $timeout(fireInitialAnimations, 100);
 
 176         // Increment our counter
 
 177         initialAnimationAttempts = initialAnimationAttempts + 1;
 
 181     function enableKeyboard() {
 
 182       $element.on('keydown', keyPressed);
 
 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);
 
 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();
 
 195     function disableKeyboard() {
 
 196       $element.off('keydown', keyPressed);
 
 197       angular.element(document).off('click touchend', checkForOutsideClick);
 
 200     function checkForOutsideClick(event) {
 
 202         var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
 
 203         var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
 
 205         if (!closestTrigger && !closestActions) {
 
 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;
 
 221     function doActionPrev(event) {
 
 222       focusAction(event, -1);
 
 225     function doActionNext(event) {
 
 226       focusAction(event, 1);
 
 229     function focusAction(event, direction) {
 
 230       var actions = resetActionTabIndexes();
 
 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);
 
 238       var focusElement =  angular.element(actions[vm.currentActionIndex]).children()[0];
 
 239       angular.element(focusElement).attr('tabindex', 0);
 
 240       focusElement.focus();
 
 242       // Make sure the event doesn't bubble and cause something else
 
 243       event.preventDefault();
 
 244       event.stopImmediatePropagation();
 
 247     function resetActionTabIndexes() {
 
 248       // Grab all of the actions
 
 249       var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
 
 251       // Disable all other actions for tabbing
 
 252       angular.forEach(actions, function(action) {
 
 253         angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
 
 259     function doKeyLeft(event) {
 
 260       if (vm.direction === 'left') {
 
 267     function doKeyUp(event) {
 
 268       if (vm.direction === 'down') {
 
 275     function doKeyRight(event) {
 
 276       if (vm.direction === 'left') {
 
 283     function doKeyDown(event) {
 
 284       if (vm.direction === 'up') {
 
 291     function isTrigger(element) {
 
 292       return $mdUtil.getClosest(element, 'md-fab-trigger');
 
 295     function isAction(element) {
 
 296       return $mdUtil.getClosest(element, 'md-fab-actions');
 
 299     function handleItemClick(event) {
 
 300       if (isTrigger(event.target)) {
 
 304       if (isAction(event.target)) {
 
 309     function getTriggerElement() {
 
 310       return $element.find('md-fab-trigger');
 
 313     function getActionsElement() {
 
 314       return $element.find('md-fab-actions');
 
 323    * The duration of the CSS animation in milliseconds.
 
 327   MdFabSpeedDialFlingAnimation['$inject'] = ["$timeout"];
 
 328   MdFabSpeedDialScaleAnimation['$inject'] = ["$timeout"];
 
 329   var cssAnimationDuration = 300;
 
 333    * @name material.components.fabSpeedDial
 
 336     // Declare our module
 
 337     .module('material.components.fabSpeedDial', [
 
 339       'material.components.fabShared',
 
 340       'material.components.fabActions'
 
 343     // Register our directive
 
 344     .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
 
 346     // Register our custom animations
 
 347     .animation('.md-fling', MdFabSpeedDialFlingAnimation)
 
 348     .animation('.md-scale', MdFabSpeedDialScaleAnimation)
 
 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);
 
 356    * @name mdFabSpeedDial
 
 357    * @module material.components.fabSpeedDial
 
 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.
 
 365    * There are currently two animations available by applying one of the following classes to
 
 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%.
 
 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`
 
 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.
 
 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.
 
 387    * See the demos for more information.
 
 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.
 
 397    * <md-fab-speed-dial md-direction="up" class="md-fling">
 
 399    *     <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
 
 403    *     <md-button aria-label="Add User">
 
 404    *       <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
 
 407    *     <md-button aria-label="Add Group">
 
 408    *       <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
 
 411    * </md-fab-speed-dial>
 
 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.
 
 418   function MdFabSpeedDialDirective() {
 
 423         direction: '@?mdDirection',
 
 427       bindToController: true,
 
 428       controller: 'MdFabController',
 
 431       link: FabSpeedDialLink
 
 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>');
 
 440   function MdFabSpeedDialFlingAnimation($timeout) {
 
 441     function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
 
 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')) {
 
 450       var ctrl = element.controller('mdFabSpeedDial');
 
 451       var items = el.querySelectorAll('.md-fab-action-item');
 
 453       // Grab our trigger element
 
 454       var triggerElement = el.querySelector('md-fab-trigger');
 
 456       // Grab our element which stores CSS variables
 
 457       var variablesElement = el.querySelector('._md-css-variables');
 
 459       // Setup JS variables based on our CSS variables
 
 460       var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
 
 462       // Always reset the items to their natural position/state
 
 463       angular.forEach(items, function(item, index) {
 
 464         var styles = item.style;
 
 466         styles.transform = styles.webkitTransform = '';
 
 467         styles.transitionDelay = '';
 
 470         // Make the items closest to the trigger have the highest z-index
 
 471         styles.zIndex = (items.length - index) + startZIndex;
 
 474       // Set the trigger to be above all of the actions so they disappear behind it.
 
 475       triggerElement.style.zIndex = startZIndex + items.length + 1;
 
 477       // If the control is closed, hide the items behind the trigger
 
 479         angular.forEach(items, function(item, index) {
 
 480           var newPosition, axis;
 
 481           var styles = item.style;
 
 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
 
 486           var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
 
 487           var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
 
 489           switch (ctrl.direction) {
 
 491               newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
 
 495               newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
 
 499               newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
 
 503               newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
 
 508           var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
 
 510           styles.transform = styles.webkitTransform = newTranslate;
 
 516       addClass: function(element, className, done) {
 
 517         if (element.hasClass('md-fling')) {
 
 518           runAnimation(element);
 
 524       removeClass: function(element, className, done) {
 
 525         runAnimation(element);
 
 531   function MdFabSpeedDialScaleAnimation($timeout) {
 
 532     function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
 
 536     function runAnimation(element) {
 
 538       var ctrl = element.controller('mdFabSpeedDial');
 
 539       var items = el.querySelectorAll('.md-fab-action-item');
 
 541       // Grab our element which stores CSS variables
 
 542       var variablesElement = el.querySelector('._md-css-variables');
 
 544       // Setup JS variables based on our CSS variables
 
 545       var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
 
 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;
 
 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';
 
 556         // Make the items closest to the trigger have the highest z-index
 
 557         styles.zIndex = (items.length - index) + startZIndex;
 
 562       addClass: function(element, className, done) {
 
 563         runAnimation(element);
 
 567       removeClass: function(element, className, done) {
 
 568         runAnimation(element);
 
 575 ngmaterial.components.fabShared = angular.module("material.components.fabShared");