UNPKG

angular-material-npfixed

Version:

The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M

247 lines (212 loc) 7.95 kB
angular .module('material.components.menu') .controller('mdMenuCtrl', MenuController); /** * @ngInject */ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q, $log) { 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(); }); }; 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:not([disabled])'); 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>'); } }; // Functionality that is exposed in the view. $scope.$mdMenu = { open: this.open, close: this.close }; // Deprecated APIs $scope.$mdOpenMenu = angular.bind(this, function() { $log.warn('mdMenu: The $mdOpenMenu method is deprecated. Please use `$mdMenu.open`.'); return this.open.apply(this, arguments); }); }