1// Copyright (c) 2012 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 5cr.define('cr.ui', function() { 6 /** @const */ 7 var Menu = cr.ui.Menu; 8 9 /** @const */ 10 var positionPopupAroundElement = cr.ui.positionPopupAroundElement; 11 12 /** 13 * Enum for type of hide. Delayed is used when called by clicking on a 14 * checkable menu item. 15 * @enum {number} 16 */ 17 var HideType = { 18 INSTANT: 0, 19 DELAYED: 1 20 }; 21 22 /** 23 * Creates a new menu button element. 24 * @param {Object=} opt_propertyBag Optional properties. 25 * @constructor 26 * @extends {HTMLButtonElement} 27 */ 28 var MenuButton = cr.ui.define('button'); 29 30 MenuButton.prototype = { 31 __proto__: HTMLButtonElement.prototype, 32 33 /** 34 * Initializes the menu button. 35 */ 36 decorate: function() { 37 this.addEventListener('mousedown', this); 38 this.addEventListener('keydown', this); 39 40 // Adding the 'custom-appearance' class prevents widgets.css from changing 41 // the appearance of this element. 42 this.classList.add('custom-appearance'); 43 this.classList.add('menu-button'); // For styles in menu_button.css. 44 45 var menu; 46 if ((menu = this.getAttribute('menu'))) 47 this.menu = menu; 48 49 // An event tracker for events we only connect to while the menu is 50 // displayed. 51 this.showingEvents_ = new EventTracker(); 52 53 this.anchorType = cr.ui.AnchorType.BELOW; 54 this.invertLeftRight = false; 55 }, 56 57 /** 58 * The menu associated with the menu button. 59 * @type {cr.ui.Menu} 60 */ 61 get menu() { 62 return this.menu_; 63 }, 64 set menu(menu) { 65 if (typeof menu == 'string' && menu[0] == '#') { 66 menu = this.ownerDocument.getElementById(menu.slice(1)); 67 cr.ui.decorate(menu, Menu); 68 } 69 70 this.menu_ = menu; 71 if (menu) { 72 if (menu.id) 73 this.setAttribute('menu', '#' + menu.id); 74 } 75 }, 76 77 /** 78 * Handles event callbacks. 79 * @param {Event} e The event object. 80 */ 81 handleEvent: function(e) { 82 if (!this.menu) 83 return; 84 85 switch (e.type) { 86 case 'mousedown': 87 if (e.currentTarget == this.ownerDocument) { 88 if (!this.contains(e.target) && !this.menu.contains(e.target)) 89 this.hideMenu(); 90 else 91 e.preventDefault(); 92 } else { 93 if (this.isMenuShown()) { 94 this.hideMenu(); 95 } else if (e.button == 0) { // Only show the menu when using left 96 // mouse button. 97 this.showMenu(false); 98 99 // Prevent the button from stealing focus on mousedown. 100 e.preventDefault(); 101 } 102 } 103 104 // Hide the focus ring on mouse click. 105 this.classList.add('using-mouse'); 106 break; 107 case 'keydown': 108 this.handleKeyDown(e); 109 // If the menu is visible we let it handle all the keyboard events. 110 if (this.isMenuShown() && e.currentTarget == this.ownerDocument) { 111 if (this.menu.handleKeyDown(e)) { 112 e.preventDefault(); 113 e.stopPropagation(); 114 } 115 } 116 117 // Show the focus ring on keypress. 118 this.classList.remove('using-mouse'); 119 break; 120 case 'focus': 121 if (!this.contains(e.target) && !this.menu.contains(e.target)) { 122 this.hideMenu(); 123 // Show the focus ring on focus - if it's come from a mouse event, 124 // the focus ring will be hidden in the mousedown event handler, 125 // executed after this. 126 this.classList.remove('using-mouse'); 127 } 128 break; 129 case 'activate': 130 var hideDelayed = e.target instanceof cr.ui.MenuItem && 131 e.target.checkable; 132 this.hideMenu(hideDelayed ? HideType.DELAYED : HideType.INSTANT); 133 break; 134 case 'scroll': 135 if (!(e.target == this.menu || this.menu.contains(e.target))) 136 this.hideMenu(); 137 break; 138 case 'popstate': 139 case 'resize': 140 this.hideMenu(); 141 break; 142 } 143 }, 144 145 /** 146 * Shows the menu. 147 * @param {boolean} shouldSetFocus Whether to set focus on the 148 * selected menu item. 149 */ 150 showMenu: function(shouldSetFocus) { 151 this.hideMenu(); 152 153 this.menu.updateCommands(this); 154 155 var event = document.createEvent('UIEvents'); 156 event.initUIEvent('menushow', true, true, window, null); 157 158 if (!this.dispatchEvent(event)) 159 return; 160 161 this.menu.hidden = false; 162 163 this.setAttribute('menu-shown', ''); 164 165 // When the menu is shown we steal all keyboard events. 166 var doc = this.ownerDocument; 167 var win = doc.defaultView; 168 this.showingEvents_.add(doc, 'keydown', this, true); 169 this.showingEvents_.add(doc, 'mousedown', this, true); 170 this.showingEvents_.add(doc, 'focus', this, true); 171 this.showingEvents_.add(doc, 'scroll', this, true); 172 this.showingEvents_.add(win, 'popstate', this); 173 this.showingEvents_.add(win, 'resize', this); 174 this.showingEvents_.add(this.menu, 'activate', this); 175 this.positionMenu_(); 176 177 if (shouldSetFocus) 178 this.menu.focusSelectedItem(); 179 }, 180 181 /** 182 * Hides the menu. If your menu can go out of scope, make sure to call this 183 * first. 184 * @param {HideType=} opt_hideType Type of hide. 185 * default: HideType.INSTANT. 186 */ 187 hideMenu: function(opt_hideType) { 188 if (!this.isMenuShown()) 189 return; 190 191 this.removeAttribute('menu-shown'); 192 if (opt_hideType == HideType.DELAYED) 193 this.menu.classList.add('hide-delayed'); 194 else 195 this.menu.classList.remove('hide-delayed'); 196 this.menu.hidden = true; 197 198 this.showingEvents_.removeAll(); 199 this.focus(); 200 }, 201 202 /** 203 * Whether the menu is shown. 204 */ 205 isMenuShown: function() { 206 return this.hasAttribute('menu-shown'); 207 }, 208 209 /** 210 * Positions the menu below the menu button. At this point we do not use any 211 * advanced positioning logic to ensure the menu fits in the viewport. 212 * @private 213 */ 214 positionMenu_: function() { 215 positionPopupAroundElement(this, this.menu, this.anchorType, 216 this.invertLeftRight); 217 }, 218 219 /** 220 * Handles the keydown event for the menu button. 221 */ 222 handleKeyDown: function(e) { 223 switch (e.keyIdentifier) { 224 case 'Down': 225 case 'Up': 226 case 'Enter': 227 case 'U+0020': // Space 228 if (!this.isMenuShown()) 229 this.showMenu(true); 230 e.preventDefault(); 231 break; 232 case 'Esc': 233 case 'U+001B': // Maybe this is remote desktop playing a prank? 234 case 'U+0009': // Tab 235 this.hideMenu(); 236 break; 237 } 238 } 239 }; 240 241 /** 242 * Helper for styling a menu button with a drop-down arrow indicator. 243 * Creates a new 2D canvas context and draws a downward-facing arrow into it. 244 * @param {string} canvasName The name of the canvas. The canvas can be 245 * addressed from CSS using -webkit-canvas(<canvasName>). 246 * @param {number} width The width of the canvas and the arrow. 247 * @param {number} height The height of the canvas and the arrow. 248 * @param {string} colorSpec The CSS color to use when drawing the arrow. 249 */ 250 function createDropDownArrowCanvas(canvasName, width, height, colorSpec) { 251 var ctx = document.getCSSCanvasContext('2d', canvasName, width, height); 252 ctx.fillStyle = ctx.strokeStyle = colorSpec; 253 ctx.beginPath(); 254 ctx.moveTo(0, 0); 255 ctx.lineTo(width, 0); 256 ctx.lineTo(height, height); 257 ctx.closePath(); 258 ctx.fill(); 259 ctx.stroke(); 260 }; 261 262 /** @const */ var ARROW_WIDTH = 6; 263 /** @const */ var ARROW_HEIGHT = 3; 264 265 /** 266 * Create the images used to style drop-down-style MenuButtons. 267 * This should be called before creating any MenuButtons that will have the 268 * CSS class 'drop-down'. If no colors are specified, defaults will be used. 269 * @param {=string} normalColor CSS color for the default button state. 270 * @param {=string} hoverColor CSS color for the hover button state. 271 * @param {=string} activeColor CSS color for the active button state. 272 */ 273 MenuButton.createDropDownArrows = function( 274 normalColor, hoverColor, activeColor) { 275 normalColor = normalColor || 'rgb(192, 195, 198)'; 276 hoverColor = hoverColor || 'rgb(48, 57, 66)'; 277 activeColor = activeColor || 'white'; 278 279 createDropDownArrowCanvas( 280 'drop-down-arrow', ARROW_WIDTH, ARROW_HEIGHT, normalColor); 281 createDropDownArrowCanvas( 282 'drop-down-arrow-hover', ARROW_WIDTH, ARROW_HEIGHT, hoverColor); 283 createDropDownArrowCanvas( 284 'drop-down-arrow-active', ARROW_WIDTH, ARROW_HEIGHT, activeColor); 285 }; 286 287 // Export 288 return { 289 MenuButton: MenuButton, 290 HideType: HideType 291 }; 292}); 293