|
|
/*! * Angular Material Design * https://github.com/angular/material
* @license MIT * v1.1.1 */ (function( window, angular, undefined ){ "use strict";
/** * @ngdoc module * @name material.components.menu */
angular.module('material.components.menu', [ 'material.core', 'material.components.backdrop' ]);
MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q"]; angular .module('material.components.menu') .controller('mdMenuCtrl', MenuController);
/** * ngInject */ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q) {
var prefixer = $mdUtil.prefixer(); var menuContainer; var self = this; var triggerElement;
this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
/** * Called by our linking fn to provide access to the menu-content * element removed during link */ this.init = function init(setMenuContainer, opts) { opts = opts || {}; menuContainer = setMenuContainer;
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])); triggerElement.setAttribute('aria-expanded', 'false');
this.isInMenuBar = opts.isInMenuBar; this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
menuContainer.on('$mdInterimElementRemove', function() { self.isOpen = false; $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); }); $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
var menuContainerId = 'menu_container_' + $mdUtil.nextUid(); menuContainer.attr('id', menuContainerId); angular.element(triggerElement).attr({ 'aria-owns': menuContainerId, 'aria-haspopup': 'true' });
$scope.$on('$destroy', angular.bind(this, function() { this.disableHoverListener(); $mdMenu.destroy(); }));
menuContainer.on('$destroy', function() { $mdMenu.destroy(); }); };
var openMenuTimeout, menuItems, deregisterScopeListeners = []; this.enableHoverListener = function() { deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) { if (menuContainer[0].contains(el[0])) { self.currentlyOpenMenu = el.controller('mdMenu'); self.isAlreadyOpening = false; self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self)); } })); deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) { if (menuContainer[0].contains(el[0])) { self.currentlyOpenMenu = undefined; } })); menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children)); menuItems.on('mouseenter', self.handleMenuItemHover); menuItems.on('mouseleave', self.handleMenuItemMouseLeave); };
this.disableHoverListener = function() { while (deregisterScopeListeners.length) { deregisterScopeListeners.shift()(); } menuItems && menuItems.off('mouseenter', self.handleMenuItemHover); menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave); };
this.handleMenuItemHover = function(event) { if (self.isAlreadyOpening) return; var nestedMenu = ( event.target.querySelector('md-menu') || $mdUtil.getClosest(event.target, 'MD-MENU') ); openMenuTimeout = $timeout(function() { if (nestedMenu) { nestedMenu = angular.element(nestedMenu).controller('mdMenu'); }
if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) { var closeTo = self.nestLevel + 1; self.currentlyOpenMenu.close(true, { closeTo: closeTo }); self.isAlreadyOpening = !!nestedMenu; nestedMenu && nestedMenu.open(); } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) { self.isAlreadyOpening = !!nestedMenu; nestedMenu && nestedMenu.open(); } }, nestedMenu ? 100 : 250); var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])'); focusableTarget && focusableTarget.focus(); };
this.handleMenuItemMouseLeave = function() { if (openMenuTimeout) { $timeout.cancel(openMenuTimeout); openMenuTimeout = undefined; } };
/** * Uses the $mdMenu interim element service to open the menu contents */ this.open = function openMenu(ev) { ev && ev.stopPropagation(); ev && ev.preventDefault(); if (self.isOpen) return; self.enableHoverListener(); self.isOpen = true; $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);}); triggerElement = triggerElement || (ev ? ev.target : $element[0]); triggerElement.setAttribute('aria-expanded', 'true'); $scope.$emit('$mdMenuOpen', $element); $mdMenu.show({ scope: $scope, mdMenuCtrl: self, nestLevel: self.nestLevel, element: menuContainer, target: triggerElement, preserveElement: true, parent: 'body' }).finally(function() { triggerElement.setAttribute('aria-expanded', 'false'); self.disableHoverListener(); }); };
// Expose a open function to the child scope for html to use
$scope.$mdOpenMenu = this.open;
this.onIsOpenChanged = function(isOpen) { if (isOpen) { menuContainer.attr('aria-hidden', 'false'); $element[0].classList.add('md-open'); angular.forEach(self.nestedMenus, function(el) { el.classList.remove('md-open'); }); } else { menuContainer.attr('aria-hidden', 'true'); $element[0].classList.remove('md-open'); } $scope.$mdMenuIsOpen = self.isOpen; };
this.focusMenuContainer = function focusMenuContainer() { var focusTarget = menuContainer[0] .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button'); focusTarget.focus(); };
this.registerContainerProxy = function registerContainerProxy(handler) { this.containerProxy = handler; };
this.triggerContainerProxy = function triggerContainerProxy(ev) { this.containerProxy && this.containerProxy(ev); };
this.destroy = function() { return self.isOpen ? $mdMenu.destroy() : $q.when(false); };
// Use the $mdMenu interim element service to close the menu contents
this.close = function closeMenu(skipFocus, closeOpts) { if ( !self.isOpen ) return; self.isOpen = false; $mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus }); $scope.$emit('$mdMenuClose', $element, eventDetails); $mdMenu.hide(null, closeOpts);
if (!skipFocus) { var el = self.restoreFocusTo || $element.find('button')[0]; if (el instanceof angular.element) el = el[0]; if (el) el.focus(); } };
/** * Build a nice object out of our string attribute which specifies the * target mode for left and top positioning */ this.positionMode = function positionMode() { var attachment = ($attrs.mdPositionMode || 'target').split(' ');
// If attachment is a single item, duplicate it for our second value.
// ie. 'target' -> 'target target'
if (attachment.length == 1) { attachment.push(attachment[0]); }
return { left: attachment[0], top: attachment[1] }; };
/** * Build a nice object out of our string attribute which specifies * the offset of top and left in pixels. */ this.offsets = function offsets() { var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat); if (position.length == 2) { return { left: position[0], top: position[1] }; } else if (position.length == 1) { return { top: position[0], left: position[0] }; } else { throw Error('Invalid offsets specified. Please follow format <x, y> or <n>'); } }; }
/** * @ngdoc directive * @name mdMenu * @module material.components.menu * @restrict E * @description * * Menus are elements that open when clicked. They are useful for displaying * additional options within the context of an action. * * Every `md-menu` must specify exactly two child elements. The first element is what is * left in the DOM and is used to open the menu. This element is called the trigger element. * The trigger element's scope has access to `$mdOpenMenu($event)` * which it may call to open the menu. By passing $event as argument, the * corresponding event is stopped from propagating up the DOM-tree. * * The second element is the `md-menu-content` element which represents the * contents of the menu when it is open. Typically this will contain `md-menu-item`s, * but you can do custom content as well. * * <hljs lang="html"> * <md-menu> * <!-- Trigger element is a md-button with an icon --> * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open sample menu"> * <md-icon md-svg-icon="call:phone"></md-icon> * </md-button> * <md-menu-content> * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> * </md-menu-content> * </md-menu> * </hljs>
* ## Sizing Menus * * The width of the menu when it is open may be specified by specifying a `width` * attribute on the `md-menu-content` element. * See the [Material Design Spec](https://material.google.com/components/menus.html#menus-simple-menus)
* for more information. * * * ## Aligning Menus * * When a menu opens, it is important that the content aligns with the trigger element. * Failure to align menus can result in jarring experiences for users as content * suddenly shifts. To help with this, `md-menu` provides serveral APIs to help * with alignment. * * ### Target Mode * * By default, `md-menu` will attempt to align the `md-menu-content` by aligning * designated child elements in both the trigger and the menu content. * * To specify the alignment element in the `trigger` you can use the `md-menu-origin` * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu` * will be used as the origin element. * * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a * `md-menu-item` to specify the node that it should try and align with. * * In this example code, we specify an icon to be our origin element, and an * icon in our menu content to be our alignment target. This ensures that both * icons are aligned when the menu opens. * * <hljs lang="html"> * <md-menu> * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open some menu"> * <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon> * </md-button> * <md-menu-content> * <md-menu-item> * <md-button ng-click="doSomething()" aria-label="Do something"> * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> * Do Something * </md-button> * </md-menu-item> * </md-menu-content> * </md-menu> * </hljs> * * Sometimes we want to specify alignment on the right side of an element, for example * if we have a menu on the right side a toolbar, we want to right align our menu content. * * We can specify the origin by using the `md-position-mode` attribute on both * the `x` and `y` axis. Right now only the `x-axis` has more than one option. * You may specify the default mode of `target target` or * `target-right target` to specify a right-oriented alignment target. See the * position section of the demos for more examples. * * ### Menu Offsets * * It is sometimes unavoidable to need to have a deeper level of control for * the positioning of a menu to ensure perfect alignment. `md-menu` provides * the `md-offset` attribute to allow pixel level specificty of adjusting the * exact positioning. * * This offset is provided in the format of `x y` or `n` where `n` will be used * in both the `x` and `y` axis. * * For example, to move a menu by `2px` from the top, we can use: * <hljs lang="html"> * <md-menu md-offset="2 0"> * <!-- menu-content --> * </md-menu> * </hljs> * * ### Auto Focus * By default, when a menu opens, `md-menu` focuses the first button in the menu content. * * But sometimes you would like to focus another specific menu item instead of the first.<br/> * This can be done by applying the `md-autofocus` directive on the given element. * * <hljs lang="html"> * <md-menu-item> * <md-button md-autofocus ng-click="doSomething()"> * Auto Focus * </md-button> * </md-menu-item> * </hljs> * * * ### Preventing close * * Sometimes you would like to be able to click on a menu item without having the menu * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which * can be added to a button inside a menu to stop the menu from automatically closing. * You can then close the menu programatically by injecting `$mdMenu` and calling * `$mdMenu.hide()`. * * <hljs lang="html"> * <md-menu-item> * <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close"> * <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon> * Do Something * </md-button> * </md-menu-item> * </hljs> * * @usage * <hljs lang="html"> * <md-menu> * <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button"> * <md-icon md-svg-icon="call:phone"></md-icon> * </md-button> * <md-menu-content> * <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item> * </md-menu-content> * </md-menu> * </hljs> * * @param {string} md-position-mode The position mode in the form of * `x`, `y`. Default value is `target`,`target`. Right now the `x` axis * also suppports `target-right`. * @param {string} md-offset An offset to apply to the dropdown after positioning * `x`, `y`. Default value is `0`,`0`. * */
MenuDirective.$inject = ["$mdUtil"]; angular .module('material.components.menu') .directive('mdMenu', MenuDirective);
/** * ngInject */ function MenuDirective($mdUtil) { var INVALID_PREFIX = 'Invalid HTML for md-menu: '; return { restrict: 'E', require: ['mdMenu', '?^mdMenuBar'], controller: 'mdMenuCtrl', // empty function to be built by link
scope: true, compile: compile };
function compile(templateElement) { templateElement.addClass('md-menu'); var triggerElement = templateElement.children()[0]; var prefixer = $mdUtil.prefixer();
if (!prefixer.hasAttribute(triggerElement, 'ng-click')) { triggerElement = triggerElement .querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerElement; } if (triggerElement && ( triggerElement.nodeName == 'MD-BUTTON' || triggerElement.nodeName == 'BUTTON' ) && !triggerElement.hasAttribute('type')) { triggerElement.setAttribute('type', 'button'); }
if (templateElement.children().length != 2) { throw Error(INVALID_PREFIX + 'Expected two children elements.'); }
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
var nestedMenus = templateElement[0].querySelectorAll('md-menu'); var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0; if (nestedMenus) { angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) { if (!menuEl.hasAttribute('md-position-mode')) { menuEl.setAttribute('md-position-mode', 'cascade'); } menuEl.classList.add('_md-nested-menu'); menuEl.setAttribute('md-nest-level', nestingDepth + 1); }); } return link; }
function link(scope, element, attr, ctrls) { var mdMenuCtrl = ctrls[0]; var isInMenuBar = ctrls[1] != undefined; // Move everything into a md-menu-container and pass it to the controller
var menuContainer = angular.element( '<div class="_md md-open-menu-container md-whiteframe-z2"></div>'); var menuContents = element.children()[1];
element.addClass('_md'); // private md component indicator for styling
if (!menuContents.hasAttribute('role')) { menuContents.setAttribute('role', 'menu'); } menuContainer.append(menuContents);
element.on('$destroy', function() { menuContainer.remove(); });
element.append(menuContainer); menuContainer[0].style.display = 'none'; mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
} }
MenuProvider.$inject = ["$$interimElementProvider"];angular .module('material.components.menu') .provider('$mdMenu', MenuProvider);
/* * Interim element provider for the menu. * Handles behavior for a menu while it is open, including: * - handling animating the menu opening/closing * - handling key/mouse events on the menu element * - handling enabling/disabling scroll while the menu is open * - handling redrawing during resizes and orientation changes * */
function MenuProvider($$interimElementProvider) { menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate"]; var MENU_EDGE_MARGIN = 8;
return $$interimElementProvider('$mdMenu') .setDefaults({ methods: ['target'], options: menuDefaultOptions });
/* ngInject */ function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) { var prefixer = $mdUtil.prefixer(); var animator = $mdUtil.dom.animator;
return { parent: 'body', onShow: onShow, onRemove: onRemove, hasBackdrop: true, disableParentScroll: true, skipCompile: true, preserveScope: true, skipHide: true, themable: true };
/** * Show modal backdrop element... * @returns {function(): void} A function that removes this backdrop */ function showBackdrop(scope, element, options) { if (options.nestLevel) return angular.noop;
// If we are not within a dialog...
if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { // !! DO this before creating the backdrop; since disableScrollAround()
// configures the scroll offset; which is used by mdBackDrop postLink()
options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); } else { options.disableParentScroll = false; }
if (options.hasBackdrop) { options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
$animate.enter(options.backdrop, $document[0].body); }
/** * Hide and destroys the backdrop created by showBackdrop() */ return function hideBackdrop() { if (options.backdrop) options.backdrop.remove(); if (options.disableParentScroll) options.restoreScroll(); }; }
/** * Removing the menu element from the DOM and remove all associated event listeners * and backdrop */ function onRemove(scope, element, opts) { opts.cleanupInteraction && opts.cleanupInteraction(); opts.cleanupResizing(); opts.hideBackdrop();
// For navigation $destroy events, do a quick, non-animated removal,
// but for normal closes (from clicks, etc) animate the removal
return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
/** * For normal closes, animate the removal. * For forced closes (like $destroy events), skip the animations */ function animateRemoval() { return $animateCss(element, {addClass: 'md-leave'}).start(); }
/** * Detach the element */ function detachAndClean() { element.removeClass('md-active'); detachElement(element, opts); opts.alreadyOpen = false; }
}
/** * Inserts and configures the staged Menu element into the DOM, positioning it, * and wiring up various interaction events */ function onShow(scope, element, opts) { sanitizeAndConfigure(opts);
// Wire up theming on our menu element
$mdTheming.inherit(opts.menuContentEl, opts.target);
// Register various listeners to move menu on resize/orientation change
opts.cleanupResizing = startRepositioningOnResize(); opts.hideBackdrop = showBackdrop(scope, element, opts);
// Return the promise for when our menu is done animating in
return showMenu() .then(function(response) { opts.alreadyOpen = true; opts.cleanupInteraction = activateInteraction(); return response; });
/** * Place the menu into the DOM and call positioning related functions */ function showMenu() { opts.parent.append(element); element[0].style.display = '';
return $q(function(resolve) { var position = calculateMenuPosition(element, opts);
element.removeClass('md-leave');
// Animate the menu scaling, and opacity [from its position origin (default == top-left)]
// to normal scale.
$animateCss(element, { addClass: 'md-active', from: animator.toCss(position), to: animator.toCss({transform: ''}) }) .start() .then(resolve);
}); }
/** * Check for valid opts and set some sane defaults */ function sanitizeAndConfigure() { if (!opts.target) { throw Error( '$mdMenu.show() expected a target to animate from in options.target' ); } angular.extend(opts, { alreadyOpen: false, isRemoved: false, target: angular.element(opts.target), //make sure it's not a naked dom node
parent: angular.element(opts.parent), menuContentEl: angular.element(element[0].querySelector('md-menu-content')) }); }
/** * Configure various resize listeners for screen changes */ function startRepositioningOnResize() {
var repositionMenu = (function(target, options) { return $$rAF.throttle(function() { if (opts.isRemoved) return; var position = calculateMenuPosition(target, options);
target.css(animator.toCss(position)); }); })(element, opts);
$window.addEventListener('resize', repositionMenu); $window.addEventListener('orientationchange', repositionMenu);
return function stopRepositioningOnResize() {
// Disable resizing handlers
$window.removeEventListener('resize', repositionMenu); $window.removeEventListener('orientationchange', repositionMenu);
} }
/** * Activate interaction on the menu. Wire up keyboard listerns for * clicks, keypresses, backdrop closing, etc. */ function activateInteraction() { element.addClass('md-clickable');
// close on backdrop click
if (opts.backdrop) opts.backdrop.on('click', onBackdropClick);
// Wire up keyboard listeners.
// - Close on escape,
// - focus next item on down arrow,
// - focus prev item on up
opts.menuContentEl.on('keydown', onMenuKeyDown); opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
// kick off initial focus in the menu on the first element
var focusTarget = opts.menuContentEl[0] .querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
if ( !focusTarget ) { var firstChild = opts.menuContentEl[0].firstElementChild;
focusTarget = firstChild && (firstChild.querySelector('.md-button:not([disabled])') || firstChild.firstElementChild); }
focusTarget && focusTarget.focus();
return function cleanupInteraction() { element.removeClass('md-clickable'); if (opts.backdrop) opts.backdrop.off('click', onBackdropClick); opts.menuContentEl.off('keydown', onMenuKeyDown); opts.menuContentEl[0].removeEventListener('click', captureClickListener, true); };
// ************************************
// internal functions
// ************************************
function onMenuKeyDown(ev) { var handled; switch (ev.keyCode) { case $mdConstant.KEY_CODE.ESCAPE: opts.mdMenuCtrl.close(false, { closeAll: true }); handled = true; break; case $mdConstant.KEY_CODE.UP_ARROW: if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) { opts.mdMenuCtrl.triggerContainerProxy(ev); } handled = true; break; case $mdConstant.KEY_CODE.DOWN_ARROW: if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) { opts.mdMenuCtrl.triggerContainerProxy(ev); } handled = true; break; case $mdConstant.KEY_CODE.LEFT_ARROW: if (opts.nestLevel) { opts.mdMenuCtrl.close(); } else { opts.mdMenuCtrl.triggerContainerProxy(ev); } handled = true; break; case $mdConstant.KEY_CODE.RIGHT_ARROW: var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU'); if (parentMenu && parentMenu != opts.parent[0]) { ev.target.click(); } else { opts.mdMenuCtrl.triggerContainerProxy(ev); } handled = true; break; } if (handled) { ev.preventDefault(); ev.stopImmediatePropagation(); } }
function onBackdropClick(e) { e.preventDefault(); e.stopPropagation(); scope.$apply(function() { opts.mdMenuCtrl.close(true, { closeAll: true }); }); }
// Close menu on menu item click, if said menu-item is not disabled
function captureClickListener(e) { var target = e.target; // Traverse up the event until we get to the menuContentEl to see if
// there is an ng-click and that the ng-click is not disabled
do { if (target == opts.menuContentEl[0]) return; if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) || target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) { var closestMenu = $mdUtil.getClosest(target, 'MD-MENU'); if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) { close(); } break; } } while (target = target.parentNode)
function close() { scope.$apply(function() { opts.mdMenuCtrl.close(true, { closeAll: true }); }); }
function hasAnyAttribute(target, attrs) { if (!target) return false;
for (var i = 0, attr; attr = attrs[i]; ++i) { if (prefixer.hasAttribute(target, attr)) { return true; } }
return false; } }
} }
/** * Takes a keypress event and focuses the next/previous menu * item from the emitting element * @param {event} e - The origin keypress event * @param {angular.element} menuEl - The menu element * @param {object} opts - The interim element options for the mdMenu * @param {number} direction - The direction to move in (+1 = next, -1 = prev) */ function focusMenuItem(e, menuEl, opts, direction) { var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
var items = $mdUtil.nodesToArray(menuEl[0].children); var currentIndex = items.indexOf(currentItem);
// Traverse through our elements in the specified direction (+/-1) and try to
// focus them until we find one that accepts focus
var didFocus; for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) { var focusTarget = items[i].querySelector('.md-button'); didFocus = attemptFocus(focusTarget); if (didFocus) { break; } } return didFocus; }
/** * Attempts to focus an element. Checks whether that element is the currently * focused element after attempting. * @param {HTMLElement} el - the element to attempt focus on * @returns {bool} - whether the element was successfully focused */ function attemptFocus(el) { if (el && el.getAttribute('tabindex') != -1) { el.focus(); return ($document[0].activeElement == el); } }
/** * Use browser to remove this element without triggering a $destroy event */ function detachElement(element, opts) { if (!opts.preserveElement) { if (toNode(element).parentNode === toNode(opts.parent)) { toNode(opts.parent).removeChild(toNode(element)); } } else { toNode(element).style.display = 'none'; } }
/** * Computes menu position and sets the style on the menu container * @param {HTMLElement} el - the menu container element * @param {object} opts - the interim element options object */ function calculateMenuPosition(el, opts) {
var containerNode = el[0], openMenuNode = el[0].firstElementChild, openMenuNodeRect = openMenuNode.getBoundingClientRect(), boundryNode = $document[0].body, boundryNodeRect = boundryNode.getBoundingClientRect();
var menuStyle = $window.getComputedStyle(openMenuNode);
var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0], originNodeRect = originNode.getBoundingClientRect();
var bounds = { left: boundryNodeRect.left + MENU_EDGE_MARGIN, top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN, bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN, right: boundryNodeRect.right - MENU_EDGE_MARGIN };
var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 }; var positionMode = opts.mdMenuCtrl.positionMode();
if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') { alignTarget = firstVisibleChild(); if ( alignTarget ) { // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
alignTarget = alignTarget.firstElementChild || alignTarget; alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget; alignTargetRect = alignTarget.getBoundingClientRect();
existingOffsets = { top: parseFloat(containerNode.style.top || 0), left: parseFloat(containerNode.style.left || 0) }; } }
var position = {}; var transformOrigin = 'top ';
switch (positionMode.top) { case 'target': position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top; break; case 'cascade': position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top; break; case 'bottom': position.top = originNodeRect.top + originNodeRect.height; break; default: throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.'); }
var rtl = ($mdUtil.bidi() == 'rtl');
switch (positionMode.left) { case 'target': position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left; transformOrigin += rtl ? 'right' : 'left'; break; case 'target-left': position.left = originNodeRect.left; transformOrigin += 'left'; break; case 'target-right': position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right); transformOrigin += 'right'; break; case 'cascade': var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right; position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width; transformOrigin += willFitRight ? 'left' : 'right'; break; case 'right': if (rtl) { position.left = originNodeRect.right - originNodeRect.width; transformOrigin += 'left'; } else { position.left = originNodeRect.right - openMenuNodeRect.width; transformOrigin += 'right'; } break; case 'left': if (rtl) { position.left = originNodeRect.right - openMenuNodeRect.width; transformOrigin += 'right'; } else { position.left = originNodeRect.left; transformOrigin += 'left'; } break; default: throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.'); }
var offsets = opts.mdMenuCtrl.offsets(); position.top += offsets.top; position.left += offsets.left;
clamp(position);
var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100; var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
return { top: Math.round(position.top), left: Math.round(position.left), // Animate a scale out if we aren't just repositioning
transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined, transformOrigin: transformOrigin };
/** * Clamps the repositioning of the menu within the confines of * bounding element (often the screen/body) */ function clamp(pos) { pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top); pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left); }
/** * Gets the first visible child in the openMenuNode * Necessary incase menu nodes are being dynamically hidden */ function firstVisibleChild() { for (var i = 0; i < openMenuNode.children.length; ++i) { if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') { return openMenuNode.children[i]; } } } } } function toNode(el) { if (el instanceof angular.element) { el = el[0]; } return el; } }
})(window, window.angular);
|