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