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