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