socketioMEANseed/www/bower_components/angular-material/modules/closure/menu/menu.js
www/bower_components/angular-material/modules/closure/menu/menu.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
6 */
7goog.provide('ngmaterial.components.menu');
8goog.require('ngmaterial.components.backdrop');
9goog.require('ngmaterial.core');
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
1096ngmaterial.components.menu = angular.module("material.components.menu");