in src/components/menu/js/menuController.js [10:241]
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.mdMenuBarCtrl = opts.mdMenuBarCtrl;
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
};
}