arnaucube
arnaucube/commonroutesAppPublic Self-hosted repository
commonroutesApp/www/lib/angular-material/modules/js/menu/menu.js
www/lib/angular-material/modules/js/menu/menu.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/**
  11 * @ngdoc module
  12 * @name material.components.menu
  13 */
  14
  15angular.module('material.components.menu', [
  16  'material.core',
  17  'material.components.backdrop'
  18]);
  19
  20
  21
  22MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q"];
  23angular
  24    .module('material.components.menu')
  25    .controller('mdMenuCtrl', MenuController);
  26
  27/**
  28 * ngInject
  29 */
  30function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q) {
  31
  32  var prefixer = $mdUtil.prefixer();
  33  var menuContainer;
  34  var self = this;
  35  var triggerElement;
  36
  37  this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
  38
  39  /**
  40   * Called by our linking fn to provide access to the menu-content
  41   * element removed during link
  42   */
  43  this.init = function init(setMenuContainer, opts) {
  44    opts = opts || {};
  45    menuContainer = setMenuContainer;
  46
  47    // Default element for ARIA attributes has the ngClick or ngMouseenter expression
  48    triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter']));
  49    triggerElement.setAttribute('aria-expanded', 'false');
  50
  51    this.isInMenuBar = opts.isInMenuBar;
  52    this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
  53
  54    menuContainer.on('$mdInterimElementRemove', function() {
  55      self.isOpen = false;
  56      $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
  57    });
  58    $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
  59
  60    var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
  61    menuContainer.attr('id', menuContainerId);
  62    angular.element(triggerElement).attr({
  63      'aria-owns': menuContainerId,
  64      'aria-haspopup': 'true'
  65    });
  66
  67    $scope.$on('$destroy', angular.bind(this, function() {
  68      this.disableHoverListener();
  69      $mdMenu.destroy();
  70    }));
  71
  72    menuContainer.on('$destroy', function() {
  73      $mdMenu.destroy();
  74    });
  75  };
  76
  77  var openMenuTimeout, menuItems, deregisterScopeListeners = [];
  78  this.enableHoverListener = function() {
  79    deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) {
  80      if (menuContainer[0].contains(el[0])) {
  81        self.currentlyOpenMenu = el.controller('mdMenu');
  82        self.isAlreadyOpening = false;
  83        self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
  84      }
  85    }));
  86    deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) {
  87      if (menuContainer[0].contains(el[0])) {
  88        self.currentlyOpenMenu = undefined;
  89      }
  90    }));
  91    menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children));
  92    menuItems.on('mouseenter', self.handleMenuItemHover);
  93    menuItems.on('mouseleave', self.handleMenuItemMouseLeave);
  94  };
  95
  96  this.disableHoverListener = function() {
  97    while (deregisterScopeListeners.length) {
  98      deregisterScopeListeners.shift()();
  99    }
 100    menuItems && menuItems.off('mouseenter', self.handleMenuItemHover);
 101    menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave);
 102  };
 103
 104  this.handleMenuItemHover = function(event) {
 105    if (self.isAlreadyOpening) return;
 106    var nestedMenu = (
 107      event.target.querySelector('md-menu')
 108        || $mdUtil.getClosest(event.target, 'MD-MENU')
 109    );
 110    openMenuTimeout = $timeout(function() {
 111      if (nestedMenu) {
 112        nestedMenu = angular.element(nestedMenu).controller('mdMenu');
 113      }
 114
 115      if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
 116        var closeTo = self.nestLevel + 1;
 117        self.currentlyOpenMenu.close(true, { closeTo: closeTo });
 118        self.isAlreadyOpening = !!nestedMenu;
 119        nestedMenu && nestedMenu.open();
 120      } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
 121        self.isAlreadyOpening = !!nestedMenu;
 122        nestedMenu && nestedMenu.open();
 123      }
 124    }, nestedMenu ? 100 : 250);
 125    var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])');
 126    focusableTarget && focusableTarget.focus();
 127  };
 128
 129  this.handleMenuItemMouseLeave = function() {
 130    if (openMenuTimeout) {
 131      $timeout.cancel(openMenuTimeout);
 132      openMenuTimeout = undefined;
 133    }
 134  };
 135
 136
 137  /**
 138   * Uses the $mdMenu interim element service to open the menu contents
 139   */
 140  this.open = function openMenu(ev) {
 141    ev && ev.stopPropagation();
 142    ev && ev.preventDefault();
 143    if (self.isOpen) return;
 144    self.enableHoverListener();
 145    self.isOpen = true;
 146    $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
 147    triggerElement = triggerElement || (ev ? ev.target : $element[0]);
 148    triggerElement.setAttribute('aria-expanded', 'true');
 149    $scope.$emit('$mdMenuOpen', $element);
 150    $mdMenu.show({
 151      scope: $scope,
 152      mdMenuCtrl: self,
 153      nestLevel: self.nestLevel,
 154      element: menuContainer,
 155      target: triggerElement,
 156      preserveElement: true,
 157      parent: 'body'
 158    }).finally(function() {
 159      triggerElement.setAttribute('aria-expanded', 'false');
 160      self.disableHoverListener();
 161    });
 162  };
 163
 164  // Expose a open function to the child scope for html to use
 165  $scope.$mdOpenMenu = this.open;
 166
 167  this.onIsOpenChanged = function(isOpen) {
 168    if (isOpen) {
 169      menuContainer.attr('aria-hidden', 'false');
 170      $element[0].classList.add('md-open');
 171      angular.forEach(self.nestedMenus, function(el) {
 172        el.classList.remove('md-open');
 173      });
 174    } else {
 175      menuContainer.attr('aria-hidden', 'true');
 176      $element[0].classList.remove('md-open');
 177    }
 178    $scope.$mdMenuIsOpen = self.isOpen;
 179  };
 180
 181  this.focusMenuContainer = function focusMenuContainer() {
 182    var focusTarget = menuContainer[0]
 183      .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
 184
 185    if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button');
 186    focusTarget.focus();
 187  };
 188
 189  this.registerContainerProxy = function registerContainerProxy(handler) {
 190    this.containerProxy = handler;
 191  };
 192
 193  this.triggerContainerProxy = function triggerContainerProxy(ev) {
 194    this.containerProxy && this.containerProxy(ev);
 195  };
 196
 197  this.destroy = function() {
 198    return self.isOpen ? $mdMenu.destroy() : $q.when(false);
 199  };
 200
 201  // Use the $mdMenu interim element service to close the menu contents
 202  this.close = function closeMenu(skipFocus, closeOpts) {
 203    if ( !self.isOpen ) return;
 204    self.isOpen = false;
 205    $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
 206
 207    var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus });
 208    $scope.$emit('$mdMenuClose', $element, eventDetails);
 209    $mdMenu.hide(null, closeOpts);
 210
 211    if (!skipFocus) {
 212      var el = self.restoreFocusTo || $element.find('button')[0];
 213      if (el instanceof angular.element) el = el[0];
 214      if (el) el.focus();
 215    }
 216  };
 217
 218  /**
 219   * Build a nice object out of our string attribute which specifies the
 220   * target mode for left and top positioning
 221   */
 222  this.positionMode = function positionMode() {
 223    var attachment = ($attrs.mdPositionMode || 'target').split(' ');
 224
 225    // If attachment is a single item, duplicate it for our second value.
 226    // ie. 'target' -> 'target target'
 227    if (attachment.length == 1) {
 228      attachment.push(attachment[0]);
 229    }
 230
 231    return {
 232      left: attachment[0],
 233      top: attachment[1]
 234    };
 235  };
 236
 237  /**
 238   * Build a nice object out of our string attribute which specifies
 239   * the offset of top and left in pixels.
 240   */
 241  this.offsets = function offsets() {
 242    var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
 243    if (position.length == 2) {
 244      return {
 245        left: position[0],
 246        top: position[1]
 247      };
 248    } else if (position.length == 1) {
 249      return {
 250        top: position[0],
 251        left: position[0]
 252      };
 253    } else {
 254      throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
 255    }
 256  };
 257}
 258
 259/**
 260 * @ngdoc directive
 261 * @name mdMenu
 262 * @module material.components.menu
 263 * @restrict E
 264 * @description
 265 *
 266 * Menus are elements that open when clicked. They are useful for displaying
 267 * additional options within the context of an action.
 268 *
 269 * Every `md-menu` must specify exactly two child elements. The first element is what is
 270 * left in the DOM and is used to open the menu. This element is called the trigger element.
 271 * The trigger element's scope has access to `$mdOpenMenu($event)`
 272 * which it may call to open the menu. By passing $event as argument, the
 273 * corresponding event is stopped from propagating up the DOM-tree.
 274 *
 275 * The second element is the `md-menu-content` element which represents the
 276 * contents of the menu when it is open. Typically this will contain `md-menu-item`s,
 277 * but you can do custom content as well.
 278 *
 279 * <hljs lang="html">
 280 * <md-menu>
 281 *  <!-- Trigger element is a md-button with an icon -->
 282 *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open sample menu">
 283 *    <md-icon md-svg-icon="call:phone"></md-icon>
 284 *  </md-button>
 285 *  <md-menu-content>
 286 *    <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
 287 *  </md-menu-content>
 288 * </md-menu>
 289 * </hljs>
 290
 291 * ## Sizing Menus
 292 *
 293 * The width of the menu when it is open may be specified by specifying a `width`
 294 * attribute on the `md-menu-content` element.
 295 * See the [Material Design Spec](https://material.google.com/components/menus.html#menus-simple-menus)
 296 * for more information.
 297 *
 298 *
 299 * ## Aligning Menus
 300 *
 301 * When a menu opens, it is important that the content aligns with the trigger element.
 302 * Failure to align menus can result in jarring experiences for users as content
 303 * suddenly shifts. To help with this, `md-menu` provides serveral APIs to help
 304 * with alignment.
 305 *
 306 * ### Target Mode
 307 *
 308 * By default, `md-menu` will attempt to align the `md-menu-content` by aligning
 309 * designated child elements in both the trigger and the menu content.
 310 *
 311 * To specify the alignment element in the `trigger` you can use the `md-menu-origin`
 312 * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
 313 * will be used as the origin element.
 314 *
 315 * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
 316 * `md-menu-item` to specify the node that it should try and align with.
 317 *
 318 * In this example code, we specify an icon to be our origin element, and an
 319 * icon in our menu content to be our alignment target. This ensures that both
 320 * icons are aligned when the menu opens.
 321 *
 322 * <hljs lang="html">
 323 * <md-menu>
 324 *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open some menu">
 325 *    <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
 326 *  </md-button>
 327 *  <md-menu-content>
 328 *    <md-menu-item>
 329 *      <md-button ng-click="doSomething()" aria-label="Do something">
 330 *        <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
 331 *        Do Something
 332 *      </md-button>
 333 *    </md-menu-item>
 334 *  </md-menu-content>
 335 * </md-menu>
 336 * </hljs>
 337 *
 338 * Sometimes we want to specify alignment on the right side of an element, for example
 339 * if we have a menu on the right side a toolbar, we want to right align our menu content.
 340 *
 341 * We can specify the origin by using the `md-position-mode` attribute on both
 342 * the `x` and `y` axis. Right now only the `x-axis` has more than one option.
 343 * You may specify the default mode of `target target` or
 344 * `target-right target` to specify a right-oriented alignment target. See the
 345 * position section of the demos for more examples.
 346 *
 347 * ### Menu Offsets
 348 *
 349 * It is sometimes unavoidable to need to have a deeper level of control for
 350 * the positioning of a menu to ensure perfect alignment. `md-menu` provides
 351 * the `md-offset` attribute to allow pixel level specificty of adjusting the
 352 * exact positioning.
 353 *
 354 * This offset is provided in the format of `x y` or `n` where `n` will be used
 355 * in both the `x` and `y` axis.
 356 *
 357 * For example, to move a menu by `2px` from the top, we can use:
 358 * <hljs lang="html">
 359 * <md-menu md-offset="2 0">
 360 *   <!-- menu-content -->
 361 * </md-menu>
 362 * </hljs>
 363 *
 364 * ### Auto Focus
 365 * By default, when a menu opens, `md-menu` focuses the first button in the menu content.
 366 * 
 367 * But sometimes you would like to focus another specific menu item instead of the first.<br/>
 368 * This can be done by applying the `md-autofocus` directive on the given element.
 369 *
 370 * <hljs lang="html">
 371 * <md-menu-item>
 372 *   <md-button md-autofocus ng-click="doSomething()">
 373 *     Auto Focus
 374 *   </md-button>
 375 * </md-menu-item>
 376 * </hljs>
 377 *
 378 *
 379 * ### Preventing close
 380 *
 381 * Sometimes you would like to be able to click on a menu item without having the menu
 382 * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which
 383 * can be added to a button inside a menu to stop the menu from automatically closing.
 384 * You can then close the menu programatically by injecting `$mdMenu` and calling 
 385 * `$mdMenu.hide()`.
 386 *
 387 * <hljs lang="html">
 388 * <md-menu-item>
 389 *   <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close">
 390 *     <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
 391 *     Do Something
 392 *   </md-button>
 393 * </md-menu-item>
 394 * </hljs>
 395 *
 396 * @usage
 397 * <hljs lang="html">
 398 * <md-menu>
 399 *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button">
 400 *    <md-icon md-svg-icon="call:phone"></md-icon>
 401 *  </md-button>
 402 *  <md-menu-content>
 403 *    <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
 404 *  </md-menu-content>
 405 * </md-menu>
 406 * </hljs>
 407 *
 408 * @param {string} md-position-mode The position mode in the form of
 409 *           `x`, `y`. Default value is `target`,`target`. Right now the `x` axis
 410 *           also suppports `target-right`.
 411 * @param {string} md-offset An offset to apply to the dropdown after positioning
 412 *           `x`, `y`. Default value is `0`,`0`.
 413 *
 414 */
 415
 416MenuDirective.$inject = ["$mdUtil"];
 417angular
 418    .module('material.components.menu')
 419    .directive('mdMenu', MenuDirective);
 420
 421/**
 422 * ngInject
 423 */
 424function MenuDirective($mdUtil) {
 425  var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
 426  return {
 427    restrict: 'E',
 428    require: ['mdMenu', '?^mdMenuBar'],
 429    controller: 'mdMenuCtrl', // empty function to be built by link
 430    scope: true,
 431    compile: compile
 432  };
 433
 434  function compile(templateElement) {
 435    templateElement.addClass('md-menu');
 436    var triggerElement = templateElement.children()[0];
 437    var prefixer = $mdUtil.prefixer();
 438
 439    if (!prefixer.hasAttribute(triggerElement, 'ng-click')) {
 440      triggerElement = triggerElement
 441          .querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerElement;
 442    }
 443    if (triggerElement && (
 444      triggerElement.nodeName == 'MD-BUTTON' ||
 445      triggerElement.nodeName == 'BUTTON'
 446    ) && !triggerElement.hasAttribute('type')) {
 447      triggerElement.setAttribute('type', 'button');
 448    }
 449
 450    if (templateElement.children().length != 2) {
 451      throw Error(INVALID_PREFIX + 'Expected two children elements.');
 452    }
 453
 454    // Default element for ARIA attributes has the ngClick or ngMouseenter expression
 455    triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
 456
 457    var nestedMenus = templateElement[0].querySelectorAll('md-menu');
 458    var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
 459    if (nestedMenus) {
 460      angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
 461        if (!menuEl.hasAttribute('md-position-mode')) {
 462          menuEl.setAttribute('md-position-mode', 'cascade');
 463        }
 464        menuEl.classList.add('_md-nested-menu');
 465        menuEl.setAttribute('md-nest-level', nestingDepth + 1);
 466      });
 467    }
 468    return link;
 469  }
 470
 471  function link(scope, element, attr, ctrls) {
 472    var mdMenuCtrl = ctrls[0];
 473    var isInMenuBar = ctrls[1] != undefined;
 474    // Move everything into a md-menu-container and pass it to the controller
 475    var menuContainer = angular.element( '<div class="_md md-open-menu-container md-whiteframe-z2"></div>');
 476    var menuContents = element.children()[1];
 477
 478    element.addClass('_md');     // private md component indicator for styling
 479
 480    if (!menuContents.hasAttribute('role')) {
 481      menuContents.setAttribute('role', 'menu');
 482    }
 483    menuContainer.append(menuContents);
 484
 485    element.on('$destroy', function() {
 486      menuContainer.remove();
 487    });
 488
 489    element.append(menuContainer);
 490    menuContainer[0].style.display = 'none';
 491    mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
 492
 493  }
 494}
 495
 496
 497MenuProvider.$inject = ["$$interimElementProvider"];angular
 498  .module('material.components.menu')
 499  .provider('$mdMenu', MenuProvider);
 500
 501/*
 502 * Interim element provider for the menu.
 503 * Handles behavior for a menu while it is open, including:
 504 *    - handling animating the menu opening/closing
 505 *    - handling key/mouse events on the menu element
 506 *    - handling enabling/disabling scroll while the menu is open
 507 *    - handling redrawing during resizes and orientation changes
 508 *
 509 */
 510
 511function MenuProvider($$interimElementProvider) {
 512  menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate"];
 513  var MENU_EDGE_MARGIN = 8;
 514
 515  return $$interimElementProvider('$mdMenu')
 516    .setDefaults({
 517      methods: ['target'],
 518      options: menuDefaultOptions
 519    });
 520
 521  /* ngInject */
 522  function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) {
 523    var prefixer = $mdUtil.prefixer();
 524    var animator = $mdUtil.dom.animator;
 525
 526    return {
 527      parent: 'body',
 528      onShow: onShow,
 529      onRemove: onRemove,
 530      hasBackdrop: true,
 531      disableParentScroll: true,
 532      skipCompile: true,
 533      preserveScope: true,
 534      skipHide: true,
 535      themable: true
 536    };
 537
 538    /**
 539     * Show modal backdrop element...
 540     * @returns {function(): void} A function that removes this backdrop
 541     */
 542    function showBackdrop(scope, element, options) {
 543      if (options.nestLevel) return angular.noop;
 544
 545      // If we are not within a dialog...
 546      if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
 547        // !! DO this before creating the backdrop; since disableScrollAround()
 548        //    configures the scroll offset; which is used by mdBackDrop postLink()
 549        options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
 550      } else {
 551        options.disableParentScroll = false;
 552      }
 553
 554      if (options.hasBackdrop) {
 555        options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
 556
 557        $animate.enter(options.backdrop, $document[0].body);
 558      }
 559
 560      /**
 561       * Hide and destroys the backdrop created by showBackdrop()
 562       */
 563      return function hideBackdrop() {
 564        if (options.backdrop) options.backdrop.remove();
 565        if (options.disableParentScroll) options.restoreScroll();
 566      };
 567    }
 568
 569    /**
 570     * Removing the menu element from the DOM and remove all associated event listeners
 571     * and backdrop
 572     */
 573    function onRemove(scope, element, opts) {
 574      opts.cleanupInteraction && opts.cleanupInteraction();
 575      opts.cleanupResizing();
 576      opts.hideBackdrop();
 577
 578      // For navigation $destroy events, do a quick, non-animated removal,
 579      // but for normal closes (from clicks, etc) animate the removal
 580
 581      return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
 582
 583      /**
 584       * For normal closes, animate the removal.
 585       * For forced closes (like $destroy events), skip the animations
 586       */
 587      function animateRemoval() {
 588        return $animateCss(element, {addClass: 'md-leave'}).start();
 589      }
 590
 591      /**
 592       * Detach the element
 593       */
 594      function detachAndClean() {
 595        element.removeClass('md-active');
 596        detachElement(element, opts);
 597        opts.alreadyOpen = false;
 598      }
 599
 600    }
 601
 602    /**
 603     * Inserts and configures the staged Menu element into the DOM, positioning it,
 604     * and wiring up various interaction events
 605     */
 606    function onShow(scope, element, opts) {
 607      sanitizeAndConfigure(opts);
 608
 609      // Wire up theming on our menu element
 610      $mdTheming.inherit(opts.menuContentEl, opts.target);
 611
 612      // Register various listeners to move menu on resize/orientation change
 613      opts.cleanupResizing = startRepositioningOnResize();
 614      opts.hideBackdrop = showBackdrop(scope, element, opts);
 615
 616      // Return the promise for when our menu is done animating in
 617      return showMenu()
 618        .then(function(response) {
 619          opts.alreadyOpen = true;
 620          opts.cleanupInteraction = activateInteraction();
 621          return response;
 622        });
 623
 624      /**
 625       * Place the menu into the DOM and call positioning related functions
 626       */
 627      function showMenu() {
 628        opts.parent.append(element);
 629        element[0].style.display = '';
 630
 631        return $q(function(resolve) {
 632          var position = calculateMenuPosition(element, opts);
 633
 634          element.removeClass('md-leave');
 635
 636          // Animate the menu scaling, and opacity [from its position origin (default == top-left)]
 637          // to normal scale.
 638          $animateCss(element, {
 639            addClass: 'md-active',
 640            from: animator.toCss(position),
 641            to: animator.toCss({transform: ''})
 642          })
 643          .start()
 644          .then(resolve);
 645
 646        });
 647      }
 648
 649      /**
 650       * Check for valid opts and set some sane defaults
 651       */
 652      function sanitizeAndConfigure() {
 653        if (!opts.target) {
 654          throw Error(
 655            '$mdMenu.show() expected a target to animate from in options.target'
 656          );
 657        }
 658        angular.extend(opts, {
 659          alreadyOpen: false,
 660          isRemoved: false,
 661          target: angular.element(opts.target), //make sure it's not a naked dom node
 662          parent: angular.element(opts.parent),
 663          menuContentEl: angular.element(element[0].querySelector('md-menu-content'))
 664        });
 665      }
 666
 667      /**
 668       * Configure various resize listeners for screen changes
 669       */
 670      function startRepositioningOnResize() {
 671
 672        var repositionMenu = (function(target, options) {
 673          return $$rAF.throttle(function() {
 674            if (opts.isRemoved) return;
 675            var position = calculateMenuPosition(target, options);
 676
 677            target.css(animator.toCss(position));
 678          });
 679        })(element, opts);
 680
 681        $window.addEventListener('resize', repositionMenu);
 682        $window.addEventListener('orientationchange', repositionMenu);
 683
 684        return function stopRepositioningOnResize() {
 685
 686          // Disable resizing handlers
 687          $window.removeEventListener('resize', repositionMenu);
 688          $window.removeEventListener('orientationchange', repositionMenu);
 689
 690        }
 691      }
 692
 693      /**
 694       * Activate interaction on the menu. Wire up keyboard listerns for
 695       * clicks, keypresses, backdrop closing, etc.
 696       */
 697      function activateInteraction() {
 698        element.addClass('md-clickable');
 699
 700        // close on backdrop click
 701        if (opts.backdrop) opts.backdrop.on('click', onBackdropClick);
 702
 703        // Wire up keyboard listeners.
 704        // - Close on escape,
 705        // - focus next item on down arrow,
 706        // - focus prev item on up
 707        opts.menuContentEl.on('keydown', onMenuKeyDown);
 708        opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
 709
 710        // kick off initial focus in the menu on the first element
 711        var focusTarget = opts.menuContentEl[0]
 712          .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
 713
 714        if ( !focusTarget ) {
 715          var firstChild = opts.menuContentEl[0].firstElementChild;
 716
 717          focusTarget = firstChild && (firstChild.querySelector('.md-button:not([disabled])') || firstChild.firstElementChild);
 718        }
 719
 720        focusTarget && focusTarget.focus();
 721
 722        return function cleanupInteraction() {
 723          element.removeClass('md-clickable');
 724          if (opts.backdrop) opts.backdrop.off('click', onBackdropClick);
 725          opts.menuContentEl.off('keydown', onMenuKeyDown);
 726          opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
 727        };
 728
 729        // ************************************
 730        // internal functions
 731        // ************************************
 732
 733        function onMenuKeyDown(ev) {
 734          var handled;
 735          switch (ev.keyCode) {
 736            case $mdConstant.KEY_CODE.ESCAPE:
 737              opts.mdMenuCtrl.close(false, { closeAll: true });
 738              handled = true;
 739              break;
 740            case $mdConstant.KEY_CODE.UP_ARROW:
 741              if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
 742                opts.mdMenuCtrl.triggerContainerProxy(ev);
 743              }
 744              handled = true;
 745              break;
 746            case $mdConstant.KEY_CODE.DOWN_ARROW:
 747              if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
 748                opts.mdMenuCtrl.triggerContainerProxy(ev);
 749              }
 750              handled = true;
 751              break;
 752            case $mdConstant.KEY_CODE.LEFT_ARROW:
 753              if (opts.nestLevel) {
 754                opts.mdMenuCtrl.close();
 755              } else {
 756                opts.mdMenuCtrl.triggerContainerProxy(ev);
 757              }
 758              handled = true;
 759              break;
 760            case $mdConstant.KEY_CODE.RIGHT_ARROW:
 761              var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU');
 762              if (parentMenu && parentMenu != opts.parent[0]) {
 763                ev.target.click();
 764              } else {
 765                opts.mdMenuCtrl.triggerContainerProxy(ev);
 766              }
 767              handled = true;
 768              break;
 769          }
 770          if (handled) {
 771            ev.preventDefault();
 772            ev.stopImmediatePropagation();
 773          }
 774        }
 775
 776        function onBackdropClick(e) {
 777          e.preventDefault();
 778          e.stopPropagation();
 779          scope.$apply(function() {
 780            opts.mdMenuCtrl.close(true, { closeAll: true });
 781          });
 782        }
 783
 784        // Close menu on menu item click, if said menu-item is not disabled
 785        function captureClickListener(e) {
 786          var target = e.target;
 787          // Traverse up the event until we get to the menuContentEl to see if
 788          // there is an ng-click and that the ng-click is not disabled
 789          do {
 790            if (target == opts.menuContentEl[0]) return;
 791            if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
 792                target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
 793              var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
 794              if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
 795                close();
 796              }
 797              break;
 798            }
 799          } while (target = target.parentNode)
 800
 801          function close() {
 802            scope.$apply(function() {
 803              opts.mdMenuCtrl.close(true, { closeAll: true });
 804            });
 805          }
 806
 807          function hasAnyAttribute(target, attrs) {
 808            if (!target) return false;
 809
 810            for (var i = 0, attr; attr = attrs[i]; ++i) {
 811              if (prefixer.hasAttribute(target, attr)) {
 812                return true;
 813              }
 814            }
 815
 816            return false;
 817          }
 818        }
 819
 820      }
 821    }
 822
 823    /**
 824     * Takes a keypress event and focuses the next/previous menu
 825     * item from the emitting element
 826     * @param {event} e - The origin keypress event
 827     * @param {angular.element} menuEl - The menu element
 828     * @param {object} opts - The interim element options for the mdMenu
 829     * @param {number} direction - The direction to move in (+1 = next, -1 = prev)
 830     */
 831    function focusMenuItem(e, menuEl, opts, direction) {
 832      var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
 833
 834      var items = $mdUtil.nodesToArray(menuEl[0].children);
 835      var currentIndex = items.indexOf(currentItem);
 836
 837      // Traverse through our elements in the specified direction (+/-1) and try to
 838      // focus them until we find one that accepts focus
 839      var didFocus;
 840      for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) {
 841        var focusTarget = items[i].querySelector('.md-button');
 842        didFocus = attemptFocus(focusTarget);
 843        if (didFocus) {
 844          break;
 845        }
 846      }
 847      return didFocus;
 848    }
 849
 850    /**
 851     * Attempts to focus an element. Checks whether that element is the currently
 852     * focused element after attempting.
 853     * @param {HTMLElement} el - the element to attempt focus on
 854     * @returns {bool} - whether the element was successfully focused
 855     */
 856    function attemptFocus(el) {
 857      if (el && el.getAttribute('tabindex') != -1) {
 858        el.focus();
 859        return ($document[0].activeElement == el);
 860      }
 861    }
 862
 863    /**
 864     * Use browser to remove this element without triggering a $destroy event
 865     */
 866    function detachElement(element, opts) {
 867      if (!opts.preserveElement) {
 868        if (toNode(element).parentNode === toNode(opts.parent)) {
 869          toNode(opts.parent).removeChild(toNode(element));
 870        }
 871      } else {
 872        toNode(element).style.display = 'none';
 873      }
 874    }
 875
 876    /**
 877     * Computes menu position and sets the style on the menu container
 878     * @param {HTMLElement} el - the menu container element
 879     * @param {object} opts - the interim element options object
 880     */
 881    function calculateMenuPosition(el, opts) {
 882
 883      var containerNode = el[0],
 884        openMenuNode = el[0].firstElementChild,
 885        openMenuNodeRect = openMenuNode.getBoundingClientRect(),
 886        boundryNode = $document[0].body,
 887        boundryNodeRect = boundryNode.getBoundingClientRect();
 888
 889      var menuStyle = $window.getComputedStyle(openMenuNode);
 890
 891      var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0],
 892        originNodeRect = originNode.getBoundingClientRect();
 893
 894      var bounds = {
 895        left: boundryNodeRect.left + MENU_EDGE_MARGIN,
 896        top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
 897        bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
 898        right: boundryNodeRect.right - MENU_EDGE_MARGIN
 899      };
 900
 901      var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets  = { top:0, left : 0, right:0, bottom:0  };
 902      var positionMode = opts.mdMenuCtrl.positionMode();
 903
 904      if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') {
 905        alignTarget = firstVisibleChild();
 906        if ( alignTarget ) {
 907          // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
 908          alignTarget = alignTarget.firstElementChild || alignTarget;
 909          alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget;
 910          alignTargetRect = alignTarget.getBoundingClientRect();
 911
 912          existingOffsets = {
 913            top: parseFloat(containerNode.style.top || 0),
 914            left: parseFloat(containerNode.style.left || 0)
 915          };
 916        }
 917      }
 918
 919      var position = {};
 920      var transformOrigin = 'top ';
 921
 922      switch (positionMode.top) {
 923        case 'target':
 924          position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top;
 925          break;
 926        case 'cascade':
 927          position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
 928          break;
 929        case 'bottom':
 930          position.top = originNodeRect.top + originNodeRect.height;
 931          break;
 932        default:
 933          throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.');
 934      }
 935
 936      var rtl = ($mdUtil.bidi() == 'rtl');
 937
 938      switch (positionMode.left) {
 939        case 'target':
 940          position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left;
 941          transformOrigin += rtl ? 'right'  : 'left';
 942          break;
 943        case 'target-left':
 944          position.left = originNodeRect.left;
 945          transformOrigin += 'left';
 946          break;
 947        case 'target-right':
 948          position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
 949          transformOrigin += 'right';
 950          break;
 951        case 'cascade':
 952          var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
 953          position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
 954          transformOrigin += willFitRight ? 'left' : 'right';
 955          break;
 956        case 'right':
 957          if (rtl) {
 958            position.left = originNodeRect.right - originNodeRect.width;
 959            transformOrigin += 'left';
 960          } else {
 961            position.left = originNodeRect.right - openMenuNodeRect.width;
 962            transformOrigin += 'right';
 963          }
 964          break;
 965        case 'left':
 966          if (rtl) {
 967            position.left = originNodeRect.right - openMenuNodeRect.width;
 968            transformOrigin += 'right';
 969          } else {
 970            position.left = originNodeRect.left;
 971            transformOrigin += 'left';
 972          }
 973          break;
 974        default:
 975          throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.');
 976      }
 977
 978      var offsets = opts.mdMenuCtrl.offsets();
 979      position.top += offsets.top;
 980      position.left += offsets.left;
 981
 982      clamp(position);
 983
 984      var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
 985      var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
 986
 987      return {
 988        top: Math.round(position.top),
 989        left: Math.round(position.left),
 990        // Animate a scale out if we aren't just repositioning
 991        transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined,
 992        transformOrigin: transformOrigin
 993      };
 994
 995      /**
 996       * Clamps the repositioning of the menu within the confines of
 997       * bounding element (often the screen/body)
 998       */
 999      function clamp(pos) {
1000        pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top);
1001        pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left);
1002      }
1003
1004      /**
1005       * Gets the first visible child in the openMenuNode
1006       * Necessary incase menu nodes are being dynamically hidden
1007       */
1008      function firstVisibleChild() {
1009        for (var i = 0; i < openMenuNode.children.length; ++i) {
1010          if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') {
1011            return openMenuNode.children[i];
1012          }
1013        }
1014      }
1015    }
1016  }
1017  function toNode(el) {
1018    if (el instanceof angular.element) {
1019      el = el[0];
1020    }
1021    return el;
1022  }
1023}
1024
1025})(window, window.angular);