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 */ var Command = cr.ui.Command; 7 8 /** 9 * Creates a new menu item element. 10 * @param {Object=} opt_propertyBag Optional properties. 11 * @constructor 12 * @extends {HTMLDivElement} 13 */ 14 var MenuItem = cr.ui.define('div'); 15 16 /** 17 * Creates a new menu separator element. 18 * @return {cr.ui.MenuItem} The new separator element. 19 */ 20 MenuItem.createSeparator = function() { 21 var el = cr.doc.createElement('hr'); 22 MenuItem.decorate(el); 23 return el; 24 }; 25 26 MenuItem.prototype = { 27 __proto__: HTMLButtonElement.prototype, 28 29 /** 30 * Initializes the menu item. 31 */ 32 decorate: function() { 33 var commandId; 34 if ((commandId = this.getAttribute('command'))) 35 this.command = commandId; 36 37 this.addEventListener('mouseup', this.handleMouseUp_); 38 39 // Adding the 'custom-appearance' class prevents widgets.css from changing 40 // the appearance of this element. 41 this.classList.add('custom-appearance'); 42 43 // Enable Text to Speech on the menu. Additionaly, ID has to be set, since 44 // it is used in element's aria-activedescendant attribute. 45 if (!this.isSeparator()) 46 this.setAttribute('role', 'menuitem'); 47 48 var iconUrl; 49 if ((iconUrl = this.getAttribute('icon'))) 50 this.iconUrl = iconUrl; 51 }, 52 53 /** 54 * The command associated with this menu item. If this is set to a string 55 * of the form "#element-id" then the element is looked up in the document 56 * of the command. 57 * @type {cr.ui.Command} 58 */ 59 command_: null, 60 get command() { 61 return this.command_; 62 }, 63 set command(command) { 64 if (this.command_) { 65 this.command_.removeEventListener('labelChange', this); 66 this.command_.removeEventListener('disabledChange', this); 67 this.command_.removeEventListener('hiddenChange', this); 68 this.command_.removeEventListener('checkedChange', this); 69 } 70 71 if (typeof command == 'string' && command[0] == '#') { 72 command = this.ownerDocument.getElementById(command.slice(1)); 73 cr.ui.decorate(command, Command); 74 } 75 76 this.command_ = command; 77 if (command) { 78 if (command.id) 79 this.setAttribute('command', '#' + command.id); 80 81 if (typeof command.label === 'string') 82 this.label = command.label; 83 this.disabled = command.disabled; 84 this.hidden = command.hidden; 85 86 this.command_.addEventListener('labelChange', this); 87 this.command_.addEventListener('disabledChange', this); 88 this.command_.addEventListener('hiddenChange', this); 89 this.command_.addEventListener('checkedChange', this); 90 } 91 92 this.updateShortcut_(); 93 }, 94 95 /** 96 * The text label. 97 * @type {string} 98 */ 99 get label() { 100 return this.textContent; 101 }, 102 set label(label) { 103 this.textContent = label; 104 }, 105 106 /** 107 * Menu icon. 108 * @type {string} 109 */ 110 get iconUrl() { 111 return this.style.backgroundImage; 112 }, 113 set iconUrl(url) { 114 this.style.backgroundImage = 'url(' + url + ')'; 115 }, 116 117 /** 118 * @return {boolean} Whether the menu item is a separator. 119 */ 120 isSeparator: function() { 121 return this.tagName == 'HR'; 122 }, 123 124 /** 125 * Updates shortcut text according to associated command. If command has 126 * multiple shortcuts, only first one is displayed. 127 */ 128 updateShortcut_: function() { 129 this.removeAttribute('shortcutText'); 130 131 if (!this.command_ || 132 !this.command_.shortcut || 133 this.command_.hideShortcutText) 134 return; 135 136 var shortcuts = this.command_.shortcut.split(/\s+/); 137 138 if (shortcuts.length == 0) 139 return; 140 141 var shortcut = shortcuts[0]; 142 var mods = {}; 143 var ident = ''; 144 shortcut.split('-').forEach(function(part) { 145 var partUc = part.toUpperCase(); 146 switch (partUc) { 147 case 'CTRL': 148 case 'ALT': 149 case 'SHIFT': 150 case 'META': 151 mods[partUc] = true; 152 break; 153 default: 154 console.assert(!ident, 'Shortcut has two non-modifier keys'); 155 ident = part; 156 } 157 }); 158 159 var shortcutText = ''; 160 161 // TODO(zvorygin): if more cornercases appear - optimize following 162 // code. Currently 'Enter' keystroke is passed as 'Enter', and 'Space' 163 // is passed as 'U+0020' 164 if (ident == 'U+0020') 165 ident = 'Space'; 166 167 ['CTRL', 'ALT', 'SHIFT', 'META'].forEach(function(mod) { 168 if (mods[mod]) 169 shortcutText += loadTimeData.getString('SHORTCUT_' + mod) + '+'; 170 }); 171 172 if (ident.indexOf('U+') != 0) { 173 shortcutText += 174 loadTimeData.getString('SHORTCUT_' + ident.toUpperCase()); 175 } else { 176 shortcutText += 177 String.fromCharCode(parseInt(ident.substring(2), 16)); 178 } 179 180 this.setAttribute('shortcutText', shortcutText); 181 }, 182 183 /** 184 * Handles mouseup events. This dispatches an activate event; if there is an 185 * associated command, that command is executed. 186 * @param {Event} e The mouseup event object. 187 * @private 188 */ 189 handleMouseUp_: function(e) { 190 // Only dispatch an activate event for left or middle click. 191 if (e.button > 1) 192 return; 193 194 if (!this.disabled && !this.isSeparator() && this.selected) { 195 // Store |contextElement| since it'll be removed by {Menu} on handling 196 // 'activate' event. 197 var contextElement = this.parentNode.contextElement; 198 var activationEvent = cr.doc.createEvent('Event'); 199 activationEvent.initEvent('activate', true, true); 200 activationEvent.originalEvent = e; 201 // Dispatch command event followed by executing the command object. 202 if (this.dispatchEvent(activationEvent)) { 203 var command = this.command; 204 if (command) { 205 command.execute(contextElement); 206 cr.ui.swallowDoubleClick(e); 207 } 208 } 209 } 210 }, 211 212 /** 213 * Updates command according to the node on which this menu was invoked. 214 * @param {Node=} opt_node Node on which menu was opened. 215 */ 216 updateCommand: function(opt_node) { 217 if (this.command_) { 218 this.command_.canExecuteChange(opt_node); 219 } 220 }, 221 222 /** 223 * Handles changes to the associated command. 224 * @param {Event} e The event object. 225 */ 226 handleEvent: function(e) { 227 switch (e.type) { 228 case 'disabledChange': 229 this.disabled = this.command.disabled; 230 break; 231 case 'hiddenChange': 232 this.hidden = this.command.hidden; 233 break; 234 case 'labelChange': 235 this.label = this.command.label; 236 break; 237 case 'checkedChange': 238 this.checked = this.command.checked; 239 break; 240 } 241 } 242 }; 243 244 /** 245 * Whether the menu item is disabled or not. 246 * @type {boolean} 247 */ 248 cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR); 249 250 /** 251 * Whether the menu item is hidden or not. 252 * @type {boolean} 253 */ 254 cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR); 255 256 /** 257 * Whether the menu item is selected or not. 258 * @type {boolean} 259 */ 260 cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR); 261 262 /** 263 * Whether the menu item is checked or not. 264 * @type {boolean} 265 */ 266 cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR); 267 268 /** 269 * Whether the menu item is checkable or not. 270 * @type {boolean} 271 */ 272 cr.defineProperty(MenuItem, 'checkable', cr.PropertyKind.BOOL_ATTR); 273 274 // Export 275 return { 276 MenuItem: MenuItem 277 }; 278}); 279