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/** 6 * @fileoverview A command is an abstraction of an action a user can do in the 7 * UI. 8 * 9 * When the focus changes in the document for each command a canExecute event 10 * is dispatched on the active element. By listening to this event you can 11 * enable and disable the command by setting the event.canExecute property. 12 * 13 * When a command is executed a command event is dispatched on the active 14 * element. Note that you should stop the propagation after you have handled the 15 * command if there might be other command listeners higher up in the DOM tree. 16 */ 17 18cr.define('cr.ui', function() { 19 20 /** 21 * This is used to identify keyboard shortcuts. 22 * @param {string} shortcut The text used to describe the keys for this 23 * keyboard shortcut. 24 * @constructor 25 */ 26 function KeyboardShortcut(shortcut) { 27 var mods = {}; 28 var ident = ''; 29 shortcut.split('-').forEach(function(part) { 30 var partLc = part.toLowerCase(); 31 switch (partLc) { 32 case 'alt': 33 case 'ctrl': 34 case 'meta': 35 case 'shift': 36 mods[partLc + 'Key'] = true; 37 break; 38 default: 39 if (ident) 40 throw Error('Invalid shortcut'); 41 ident = part; 42 } 43 }); 44 45 this.ident_ = ident; 46 this.mods_ = mods; 47 } 48 49 KeyboardShortcut.prototype = { 50 /** 51 * Whether the keyboard shortcut object matches a keyboard event. 52 * @param {!Event} e The keyboard event object. 53 * @return {boolean} Whether we found a match or not. 54 */ 55 matchesEvent: function(e) { 56 if (e.keyIdentifier == this.ident_) { 57 // All keyboard modifiers needs to match. 58 var mods = this.mods_; 59 return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) { 60 return e[k] == !!mods[k]; 61 }); 62 } 63 return false; 64 } 65 }; 66 67 /** 68 * Creates a new command element. 69 * @constructor 70 * @extends {HTMLElement} 71 */ 72 var Command = cr.ui.define('command'); 73 74 Command.prototype = { 75 __proto__: HTMLElement.prototype, 76 77 /** 78 * Initializes the command. 79 */ 80 decorate: function() { 81 CommandManager.init(assert(this.ownerDocument)); 82 83 if (this.hasAttribute('shortcut')) 84 this.shortcut = this.getAttribute('shortcut'); 85 }, 86 87 /** 88 * Executes the command by dispatching a command event on the given element. 89 * If |element| isn't given, the active element is used instead. 90 * If the command is {@code disabled} this does nothing. 91 * @param {HTMLElement=} opt_element Optional element to dispatch event on. 92 */ 93 execute: function(opt_element) { 94 if (this.disabled) 95 return; 96 var doc = this.ownerDocument; 97 if (doc.activeElement) { 98 var e = new Event('command', {bubbles: true}); 99 e.command = this; 100 101 (opt_element || doc.activeElement).dispatchEvent(e); 102 } 103 }, 104 105 /** 106 * Call this when there have been changes that might change whether the 107 * command can be executed or not. 108 * @param {Node=} opt_node Node for which to actuate command state. 109 */ 110 canExecuteChange: function(opt_node) { 111 dispatchCanExecuteEvent(this, 112 opt_node || this.ownerDocument.activeElement); 113 }, 114 115 /** 116 * The keyboard shortcut that triggers the command. This is a string 117 * consisting of a keyIdentifier (as reported by WebKit in keydown) as 118 * well as optional key modifiers joinded with a '-'. 119 * 120 * Multiple keyboard shortcuts can be provided by separating them by 121 * whitespace. 122 * 123 * For example: 124 * "F1" 125 * "U+0008-Meta" for Apple command backspace. 126 * "U+0041-Ctrl" for Control A 127 * "U+007F U+0008-Meta" for Delete and Command Backspace 128 * 129 * @type {string} 130 */ 131 shortcut_: '', 132 get shortcut() { 133 return this.shortcut_; 134 }, 135 set shortcut(shortcut) { 136 var oldShortcut = this.shortcut_; 137 if (shortcut !== oldShortcut) { 138 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { 139 return new KeyboardShortcut(shortcut); 140 }); 141 142 // Set this after the keyboardShortcuts_ since that might throw. 143 this.shortcut_ = shortcut; 144 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, 145 oldShortcut); 146 } 147 }, 148 149 /** 150 * Whether the event object matches the shortcut for this command. 151 * @param {!Event} e The key event object. 152 * @return {boolean} Whether it matched or not. 153 */ 154 matchesEvent: function(e) { 155 if (!this.keyboardShortcuts_) 156 return false; 157 158 return this.keyboardShortcuts_.some(function(keyboardShortcut) { 159 return keyboardShortcut.matchesEvent(e); 160 }); 161 } 162 }; 163 164 /** 165 * The label of the command. 166 */ 167 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); 168 169 /** 170 * Whether the command is disabled or not. 171 */ 172 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); 173 174 /** 175 * Whether the command is hidden or not. 176 */ 177 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); 178 179 /** 180 * Whether the command is checked or not. 181 */ 182 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); 183 184 /** 185 * The flag that prevents the shortcut text from being displayed on menu. 186 * 187 * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command) 188 * is displayed in menu when the command is assosiated with a menu item. 189 * Otherwise, no text is displayed. 190 */ 191 cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR); 192 193 /** 194 * Dispatches a canExecute event on the target. 195 * @param {!cr.ui.Command} command The command that we are testing for. 196 * @param {EventTarget} target The target element to dispatch the event on. 197 */ 198 function dispatchCanExecuteEvent(command, target) { 199 var e = new CanExecuteEvent(command); 200 target.dispatchEvent(e); 201 command.disabled = !e.canExecute; 202 } 203 204 /** 205 * The command managers for different documents. 206 */ 207 var commandManagers = {}; 208 209 /** 210 * Keeps track of the focused element and updates the commands when the focus 211 * changes. 212 * @param {!Document} doc The document that we are managing the commands for. 213 * @constructor 214 */ 215 function CommandManager(doc) { 216 doc.addEventListener('focus', this.handleFocus_.bind(this), true); 217 // Make sure we add the listener to the bubbling phase so that elements can 218 // prevent the command. 219 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); 220 } 221 222 /** 223 * Initializes a command manager for the document as needed. 224 * @param {!Document} doc The document to manage the commands for. 225 */ 226 CommandManager.init = function(doc) { 227 var uid = cr.getUid(doc); 228 if (!(uid in commandManagers)) { 229 commandManagers[uid] = new CommandManager(doc); 230 } 231 }; 232 233 CommandManager.prototype = { 234 235 /** 236 * Handles focus changes on the document. 237 * @param {Event} e The focus event object. 238 * @private 239 */ 240 handleFocus_: function(e) { 241 var target = e.target; 242 243 // Ignore focus on a menu button or command item 244 if (target.menu || target.command) 245 return; 246 247 var commands = Array.prototype.slice.call( 248 target.ownerDocument.querySelectorAll('command')); 249 250 commands.forEach(function(command) { 251 dispatchCanExecuteEvent(command, target); 252 }); 253 }, 254 255 /** 256 * Handles the keydown event and routes it to the right command. 257 * @param {!Event} e The keydown event. 258 */ 259 handleKeyDown_: function(e) { 260 var target = e.target; 261 var commands = Array.prototype.slice.call( 262 target.ownerDocument.querySelectorAll('command')); 263 264 for (var i = 0, command; command = commands[i]; i++) { 265 if (command.matchesEvent(e)) { 266 // When invoking a command via a shortcut, we have to manually check 267 // if it can be executed, since focus might not have been changed 268 // what would have updated the command's state. 269 command.canExecuteChange(); 270 271 if (!command.disabled) { 272 e.preventDefault(); 273 // We do not want any other element to handle this. 274 e.stopPropagation(); 275 command.execute(); 276 return; 277 } 278 } 279 } 280 } 281 }; 282 283 /** 284 * The event type used for canExecute events. 285 * @param {!cr.ui.Command} command The command that we are evaluating. 286 * @extends {Event} 287 * @constructor 288 * @class 289 */ 290 function CanExecuteEvent(command) { 291 var e = new Event('canExecute', {bubbles: true}); 292 e.__proto__ = CanExecuteEvent.prototype; 293 e.command = command; 294 return e; 295 } 296 297 CanExecuteEvent.prototype = { 298 __proto__: Event.prototype, 299 300 /** 301 * The current command 302 * @type {cr.ui.Command} 303 */ 304 command: null, 305 306 /** 307 * Whether the target can execute the command. Setting this also stops the 308 * propagation. 309 * @type {boolean} 310 */ 311 canExecute_: false, 312 get canExecute() { 313 return this.canExecute_; 314 }, 315 set canExecute(canExecute) { 316 this.canExecute_ = !!canExecute; 317 this.stopPropagation(); 318 } 319 }; 320 321 // Export 322 return { 323 Command: Command, 324 CanExecuteEvent: CanExecuteEvent 325 }; 326}); 327