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