1 // Copyright (c) 2010 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 18 cr.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 * Wether the keyboard shortcut object mathes 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(this.ownerDocument); 82 }, 83 84 /** 85 * Executes the command. This dispatches a command event on the active 86 * element. If the command is {@code disabled} this does nothing. 87 */ 88 execute: function() { 89 if (this.disabled) 90 return; 91 var doc = this.ownerDocument; 92 if (doc.activeElement) { 93 var e = new cr.Event('command', true, false); 94 e.command = this; 95 doc.activeElement.dispatchEvent(e); 96 } 97 }, 98 99 /** 100 * Call this when there have been changes that might change whether the 101 * command can be executed or not. 102 */ 103 canExecuteChange: function() { 104 dispatchCanExecuteEvent(this, this.ownerDocument.activeElement); 105 }, 106 107 /** 108 * The keyboard shortcut that triggers the command. This is a string 109 * consisting of a keyIdentifier (as reported by WebKit in keydown) as 110 * well as optional key modifiers joinded with a '-'. 111 * 112 * Multiple keyboard shortcuts can be provided by separating them by 113 * whitespace. 114 * 115 * For example: 116 * "F1" 117 * "U+0008-Meta" for Apple command backspace. 118 * "U+0041-Ctrl" for Control A 119 * "U+007F U+0008-Meta" for Delete and Command Backspace 120 * 121 * @type {string} 122 */ 123 shortcut_: '', 124 get shortcut() { 125 return this.shortcut_; 126 }, 127 set shortcut(shortcut) { 128 var oldShortcut = this.shortcut_; 129 if (shortcut !== oldShortcut) { 130 this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) { 131 return new KeyboardShortcut(shortcut); 132 }); 133 134 // Set this after the keyboardShortcuts_ since that might throw. 135 this.shortcut_ = shortcut; 136 cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_, 137 oldShortcut); 138 } 139 }, 140 141 /** 142 * Whether the event object matches the shortcut for this command. 143 * @param {!Event} e The key event object. 144 * @return {boolean} Whether it matched or not. 145 */ 146 matchesEvent: function(e) { 147 if (!this.keyboardShortcuts_) 148 return false; 149 150 return this.keyboardShortcuts_.some(function(keyboardShortcut) { 151 return keyboardShortcut.matchesEvent(e); 152 }); 153 } 154 }; 155 156 /** 157 * The label of the command. 158 * @type {string} 159 */ 160 cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR); 161 162 /** 163 * Whether the command is disabled or not. 164 * @type {boolean} 165 */ 166 cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR); 167 168 /** 169 * Whether the command is hidden or not. 170 * @type {boolean} 171 */ 172 cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR); 173 174 /** 175 * Whether the command is checked or not. 176 * @type {boolean} 177 */ 178 cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR); 179 180 /** 181 * Dispatches a canExecute event on the target. 182 * @param {cr.ui.Command} command The command that we are testing for. 183 * @param {Element} target The target element to dispatch the event on. 184 */ 185 function dispatchCanExecuteEvent(command, target) { 186 var e = new CanExecuteEvent(command, true) 187 target.dispatchEvent(e); 188 command.disabled = !e.canExecute; 189 } 190 191 /** 192 * The command managers for different documents. 193 */ 194 var commandManagers = {}; 195 196 /** 197 * Keeps track of the focused element and updates the commands when the focus 198 * changes. 199 * @param {!Document} doc The document that we are managing the commands for. 200 * @constructor 201 */ 202 function CommandManager(doc) { 203 doc.addEventListener('focus', this.handleFocus_.bind(this), true); 204 // Make sure we add the listener to the bubbling phase so that elements can 205 // prevent the command. 206 doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false); 207 } 208 209 /** 210 * Initializes a command manager for the document as needed. 211 * @param {!Document} doc The document to manage the commands for. 212 */ 213 CommandManager.init = function(doc) { 214 var uid = cr.getUid(doc); 215 if (!(uid in commandManagers)) { 216 commandManagers[uid] = new CommandManager(doc); 217 } 218 }, 219 220 CommandManager.prototype = { 221 222 /** 223 * Handles focus changes on the document. 224 * @param {Event} e The focus event object. 225 * @private 226 */ 227 handleFocus_: function(e) { 228 var target = e.target; 229 var commands = Array.prototype.slice.call( 230 target.ownerDocument.querySelectorAll('command')); 231 232 commands.forEach(function(command) { 233 dispatchCanExecuteEvent(command, target); 234 }); 235 }, 236 237 /** 238 * Handles the keydown event and routes it to the right command. 239 * @param {!Event} e The keydown event. 240 */ 241 handleKeyDown_: function(e) { 242 var target = e.target; 243 var commands = Array.prototype.slice.call( 244 target.ownerDocument.querySelectorAll('command')); 245 246 for (var i = 0, command; command = commands[i]; i++) { 247 if (!command.disabled && command.matchesEvent(e)) { 248 e.preventDefault(); 249 // We do not want any other element to handle this. 250 e.stopPropagation(); 251 252 command.execute(); 253 return; 254 } 255 } 256 } 257 }; 258 259 /** 260 * The event type used for canExecute events. 261 * @param {!cr.ui.Command} command The command that we are evaluating. 262 * @extends {Event} 263 */ 264 function CanExecuteEvent(command) { 265 var e = command.ownerDocument.createEvent('Event'); 266 e.initEvent('canExecute', true, false); 267 e.__proto__ = CanExecuteEvent.prototype; 268 e.command = command; 269 return e; 270 } 271 272 CanExecuteEvent.prototype = { 273 __proto__: Event.prototype, 274 275 /** 276 * The current command 277 * @type {cr.ui.Command} 278 */ 279 command: null, 280 281 /** 282 * Whether the target can execute the command. Setting this also stops the 283 * propagation. 284 * @type {boolean} 285 */ 286 canExecute_: false, 287 get canExecute() { 288 return this.canExecute_; 289 }, 290 set canExecute(canExecute) { 291 this.canExecute_ = !!canExecute; 292 this.stopPropagation(); 293 } 294 }; 295 296 // Export 297 return { 298 Command: Command, 299 CanExecuteEvent: CanExecuteEvent 300 }; 301 }); 302