1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 cr.define('cr.ui', function() { 6 const Menu = cr.ui.Menu; 7 const positionPopupAroundElement = cr.ui.positionPopupAroundElement; 8 9 /** 10 * Creates a new menu button element. 11 * @param {Object=} opt_propertyBag Optional properties. 12 * @constructor 13 * @extends {HTMLButtonElement} 14 */ 15 var MenuButton = cr.ui.define('button'); 16 17 MenuButton.prototype = { 18 __proto__: HTMLButtonElement.prototype, 19 20 /** 21 * Initializes the menu button. 22 */ 23 decorate: function() { 24 this.addEventListener('mousedown', this); 25 this.addEventListener('keydown', this); 26 27 var menu; 28 if ((menu = this.getAttribute('menu'))) 29 this.menu = menu; 30 31 // An event tracker for events we only connect to while the menu is 32 // displayed. 33 this.showingEvents_ = new EventTracker(); 34 }, 35 36 /** 37 * The menu associated with the menu button. 38 * @type {cr.ui.Menu} 39 */ 40 get menu() { 41 return this.menu_; 42 }, 43 set menu(menu) { 44 if (typeof menu == 'string' && menu[0] == '#') { 45 menu = this.ownerDocument.getElementById(menu.slice(1)); 46 cr.ui.decorate(menu, Menu); 47 } 48 49 this.menu_ = menu; 50 if (menu) { 51 if (menu.id) 52 this.setAttribute('menu', '#' + menu.id); 53 } 54 }, 55 56 /** 57 * Handles event callbacks. 58 * @param {Event} e The event object. 59 */ 60 handleEvent: function(e) { 61 if (!this.menu) 62 return; 63 64 switch (e.type) { 65 case 'mousedown': 66 if (e.currentTarget == this.ownerDocument) { 67 if (!this.contains(e.target) && !this.menu.contains(e.target)) 68 this.hideMenu(); 69 else 70 e.preventDefault(); 71 } else { 72 if (this.isMenuShown()) { 73 this.hideMenu(); 74 } else if (e.button == 0) { // Only show the menu when using left 75 // mouse button. 76 this.showMenu(); 77 // Prevent the button from stealing focus on mousedown. 78 e.preventDefault(); 79 } 80 } 81 break; 82 case 'keydown': 83 this.handleKeyDown(e); 84 // If the menu is visible we let it handle all the keyboard events. 85 if (this.isMenuShown() && e.currentTarget == this.ownerDocument) { 86 this.menu.handleKeyDown(e); 87 e.preventDefault(); 88 e.stopPropagation(); 89 } 90 break; 91 92 case 'activate': 93 case 'blur': 94 case 'resize': 95 this.hideMenu(); 96 break; 97 } 98 }, 99 100 /** 101 * Shows the menu. 102 */ 103 showMenu: function() { 104 this.hideMenu(); 105 106 this.menu.style.display = 'block'; 107 this.setAttribute('menu-shown', ''); 108 109 // when the menu is shown we steal all keyboard events. 110 var doc = this.ownerDocument; 111 var win = doc.defaultView; 112 this.showingEvents_.add(doc, 'keydown', this, true); 113 this.showingEvents_.add(doc, 'mousedown', this, true); 114 this.showingEvents_.add(doc, 'blur', this, true); 115 this.showingEvents_.add(win, 'resize', this); 116 this.showingEvents_.add(this.menu, 'activate', this); 117 this.positionMenu_(); 118 }, 119 120 /** 121 * Hides the menu. If your menu can go out of scope, make sure to call this 122 * first. 123 */ 124 hideMenu: function() { 125 if (!this.isMenuShown()) 126 return; 127 128 this.removeAttribute('menu-shown'); 129 this.menu.style.display = 'none'; 130 131 this.showingEvents_.removeAll(); 132 this.menu.selectedIndex = -1; 133 }, 134 135 /** 136 * Whether the menu is shown. 137 */ 138 isMenuShown: function() { 139 return this.hasAttribute('menu-shown'); 140 }, 141 142 /** 143 * Positions the menu below the menu button. At this point we do not use any 144 * advanced positioning logic to ensure the menu fits in the viewport. 145 * @private 146 */ 147 positionMenu_: function() { 148 positionPopupAroundElement(this, this.menu, cr.ui.AnchorType.BELOW); 149 }, 150 151 /** 152 * Handles the keydown event for the menu button. 153 */ 154 handleKeyDown: function(e) { 155 switch (e.keyIdentifier) { 156 case 'Down': 157 case 'Up': 158 case 'Enter': 159 case 'U+0020': // Space 160 if (!this.isMenuShown()) 161 this.showMenu(); 162 e.preventDefault(); 163 break; 164 case 'Esc': 165 case 'U+001B': // Maybe this is remote desktop playing a prank? 166 this.hideMenu(); 167 break; 168 } 169 } 170 }; 171 172 // Export 173 return { 174 MenuButton: MenuButton 175 }; 176 }); 177