Move Webstore URL concepts to //extensions and out
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_command_list.js
blob1f92c19cfda5f3b732e0abe71f252d58ce4dd36f
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.
5 /** @typedef {{active: boolean,
6 * command_name: string,
7 * description: string,
8 * extension_action: boolean,
9 * extension_id: string,
10 * global: boolean,
11 * keybinding: string}}
13 var ExtensionCommand;
15 cr.define('options', function() {
16 'use strict';
18 /**
19 * Creates a new list of extension commands.
20 * @param {Object=} opt_propertyBag Optional properties.
21 * @constructor
22 * @extends {HTMLDivElement}
24 var ExtensionCommandList = cr.ui.define('div');
26 /** @const */ var keyComma = 188;
27 /** @const */ var keyDel = 46;
28 /** @const */ var keyDown = 40;
29 /** @const */ var keyEnd = 35;
30 /** @const */ var keyEscape = 27;
31 /** @const */ var keyHome = 36;
32 /** @const */ var keyIns = 45;
33 /** @const */ var keyLeft = 37;
34 /** @const */ var keyMediaNextTrack = 176;
35 /** @const */ var keyMediaPlayPause = 179;
36 /** @const */ var keyMediaPrevTrack = 177;
37 /** @const */ var keyMediaStop = 178;
38 /** @const */ var keyPageDown = 34;
39 /** @const */ var keyPageUp = 33;
40 /** @const */ var keyPeriod = 190;
41 /** @const */ var keyRight = 39;
42 /** @const */ var keyTab = 9;
43 /** @const */ var keyUp = 38;
45 /**
46 * Enum for whether we require modifiers of a keycode.
47 * @enum {number}
49 var Modifiers = {
50 ARE_NOT_ALLOWED: 0,
51 ARE_REQUIRED: 1
54 /**
55 * Returns whether the passed in |keyCode| is a valid extension command
56 * char or not. This is restricted to A-Z and 0-9 (ignoring modifiers) at
57 * the moment.
58 * @param {number} keyCode The keycode to consider.
59 * @return {boolean} Returns whether the char is valid.
61 function validChar(keyCode) {
62 return keyCode == keyComma ||
63 keyCode == keyDel ||
64 keyCode == keyDown ||
65 keyCode == keyEnd ||
66 keyCode == keyHome ||
67 keyCode == keyIns ||
68 keyCode == keyLeft ||
69 keyCode == keyMediaNextTrack ||
70 keyCode == keyMediaPlayPause ||
71 keyCode == keyMediaPrevTrack ||
72 keyCode == keyMediaStop ||
73 keyCode == keyPageDown ||
74 keyCode == keyPageUp ||
75 keyCode == keyPeriod ||
76 keyCode == keyRight ||
77 keyCode == keyTab ||
78 keyCode == keyUp ||
79 (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
80 (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
83 /**
84 * Convert a keystroke event to string form, while taking into account
85 * (ignoring) invalid extension commands.
86 * @param {Event} event The keyboard event to convert.
87 * @return {string} The keystroke as a string.
89 function keystrokeToString(event) {
90 var output = '';
91 if (cr.isMac && event.metaKey)
92 output = 'Command+';
93 if (event.ctrlKey)
94 output = 'Ctrl+';
95 if (!event.ctrlKey && event.altKey)
96 output += 'Alt+';
97 if (event.shiftKey)
98 output += 'Shift+';
100 var keyCode = event.keyCode;
101 if (validChar(keyCode)) {
102 if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
103 (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) {
104 output += String.fromCharCode('A'.charCodeAt(0) + keyCode - 65);
105 } else {
106 switch (keyCode) {
107 case keyComma:
108 output += 'Comma'; break;
109 case keyDel:
110 output += 'Delete'; break;
111 case keyDown:
112 output += 'Down'; break;
113 case keyEnd:
114 output += 'End'; break;
115 case keyHome:
116 output += 'Home'; break;
117 case keyIns:
118 output += 'Insert'; break;
119 case keyLeft:
120 output += 'Left'; break;
121 case keyMediaNextTrack:
122 output += 'MediaNextTrack'; break;
123 case keyMediaPlayPause:
124 output += 'MediaPlayPause'; break;
125 case keyMediaPrevTrack:
126 output += 'MediaPrevTrack'; break;
127 case keyMediaStop:
128 output += 'MediaStop'; break;
129 case keyPageDown:
130 output += 'PageDown'; break;
131 case keyPageUp:
132 output += 'PageUp'; break;
133 case keyPeriod:
134 output += 'Period'; break;
135 case keyRight:
136 output += 'Right'; break;
137 case keyTab:
138 output += 'Tab'; break;
139 case keyUp:
140 output += 'Up'; break;
145 return output;
149 * Returns whether the passed in |keyCode| require modifiers. Currently only
150 * "MediaNextTrack", "MediaPrevTrack", "MediaStop", "MediaPlayPause" are
151 * required to be used without any modifier.
152 * @param {number} keyCode The keycode to consider.
153 * @return {Modifiers} Returns whether the keycode require modifiers.
155 function modifiers(keyCode) {
156 switch (keyCode) {
157 case keyMediaNextTrack:
158 case keyMediaPlayPause:
159 case keyMediaPrevTrack:
160 case keyMediaStop:
161 return Modifiers.ARE_NOT_ALLOWED;
162 default:
163 return Modifiers.ARE_REQUIRED;
168 * Return true if the specified keyboard event has any one of following
169 * modifiers: "Ctrl", "Alt", "Cmd" on Mac, and "Shift" when the
170 * countShiftAsModifier is true.
171 * @param {Event} event The keyboard event to consider.
172 * @param {boolean} countShiftAsModifier Whether the 'ShiftKey' should be
173 * counted as modifier.
175 function hasModifier(event, countShiftAsModifier) {
176 return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
177 (countShiftAsModifier && event.shiftKey);
180 ExtensionCommandList.prototype = {
181 __proto__: HTMLDivElement.prototype,
184 * While capturing, this records the current (last) keyboard event generated
185 * by the user. Will be |null| after capture and during capture when no
186 * keyboard event has been generated.
187 * @type {KeyboardEvent}.
188 * @private
190 currentKeyEvent_: null,
193 * While capturing, this keeps track of the previous selection so we can
194 * revert back to if no valid assignment is made during capture.
195 * @type {string}.
196 * @private
198 oldValue_: '',
201 * While capturing, this keeps track of which element the user asked to
202 * change.
203 * @type {HTMLElement}.
204 * @private
206 capturingElement_: null,
208 /** @override */
209 decorate: function() {
210 this.textContent = '';
212 // Iterate over the extension data and add each item to the list.
213 this.data_.commands.forEach(this.createNodeForExtension_.bind(this));
217 * Synthesizes and initializes an HTML element for the extension command
218 * metadata given in |extension|.
219 * @param {Object} extension A dictionary of extension metadata.
220 * @private
222 createNodeForExtension_: function(extension) {
223 var template = $('template-collection-extension-commands').querySelector(
224 '.extension-command-list-extension-item-wrapper');
225 var node = template.cloneNode(true);
227 var title = node.querySelector('.extension-title');
228 title.textContent = extension.name;
230 this.appendChild(node);
232 // Iterate over the commands data within the extension and add each item
233 // to the list.
234 extension.commands.forEach(this.createNodeForCommand_.bind(this));
238 * Synthesizes and initializes an HTML element for the extension command
239 * metadata given in |command|.
240 * @param {ExtensionCommand} command A dictionary of extension command
241 * metadata.
242 * @private
244 createNodeForCommand_: function(command) {
245 var template = $('template-collection-extension-commands').querySelector(
246 '.extension-command-list-command-item-wrapper');
247 var node = template.cloneNode(true);
248 node.id = this.createElementId_(
249 'command', command.extension_id, command.command_name);
251 var description = node.querySelector('.command-description');
252 description.textContent = command.description;
254 var shortcutNode = node.querySelector('.command-shortcut-text');
255 shortcutNode.addEventListener('mouseup',
256 this.startCapture_.bind(this));
257 shortcutNode.addEventListener('focus', this.handleFocus_.bind(this));
258 shortcutNode.addEventListener('blur', this.handleBlur_.bind(this));
259 shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this));
260 shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this));
261 if (!command.active) {
262 shortcutNode.textContent =
263 loadTimeData.getString('extensionCommandsInactive');
265 var commandShortcut = node.querySelector('.command-shortcut');
266 commandShortcut.classList.add('inactive-keybinding');
267 } else {
268 shortcutNode.textContent = command.keybinding;
271 var commandClear = node.querySelector('.command-clear');
272 commandClear.id = this.createElementId_(
273 'clear', command.extension_id, command.command_name);
274 commandClear.title = loadTimeData.getString('extensionCommandsDelete');
275 commandClear.addEventListener('click', this.handleClear_.bind(this));
277 var select = node.querySelector('.command-scope');
278 select.id = this.createElementId_(
279 'setCommandScope', command.extension_id, command.command_name);
280 select.hidden = false;
281 // Add the 'In Chrome' option.
282 var option = document.createElement('option');
283 option.textContent = loadTimeData.getString('extensionCommandsRegular');
284 select.appendChild(option);
285 if (command.extension_action) {
286 // Extension actions cannot be global, so we might as well disable the
287 // combo box, to signify that.
288 select.disabled = true;
289 } else {
290 // Add the 'Global' option.
291 option = document.createElement('option');
292 option.textContent = loadTimeData.getString('extensionCommandsGlobal');
293 select.appendChild(option);
294 select.selectedIndex = command.global ? 1 : 0;
296 select.addEventListener(
297 'change', this.handleSetCommandScope_.bind(this));
300 this.appendChild(node);
304 * Starts keystroke capture to determine which key to use for a particular
305 * extension command.
306 * @param {Event} event The keyboard event to consider.
307 * @private
309 startCapture_: function(event) {
310 if (this.capturingElement_)
311 return; // Already capturing.
313 chrome.send('setShortcutHandlingSuspended', [true]);
315 var shortcutNode = event.target;
316 this.oldValue_ = shortcutNode.textContent;
317 shortcutNode.textContent =
318 loadTimeData.getString('extensionCommandsStartTyping');
319 shortcutNode.parentElement.classList.add('capturing');
321 var commandClear =
322 shortcutNode.parentElement.querySelector('.command-clear');
323 commandClear.hidden = true;
325 this.capturingElement_ = /** @type {HTMLElement} */(event.target);
329 * Ends keystroke capture and either restores the old value or (if valid
330 * value) sets the new value as active..
331 * @param {Event} event The keyboard event to consider.
332 * @private
334 endCapture_: function(event) {
335 if (!this.capturingElement_)
336 return; // Not capturing.
338 chrome.send('setShortcutHandlingSuspended', [false]);
340 var shortcutNode = this.capturingElement_;
341 var commandShortcut = shortcutNode.parentElement;
343 commandShortcut.classList.remove('capturing');
344 commandShortcut.classList.remove('contains-chars');
346 // When the capture ends, the user may have not given a complete and valid
347 // input (or even no input at all). Only a valid key event followed by a
348 // valid key combination will cause a shortcut selection to be activated.
349 // If no valid selection was made, however, revert back to what the
350 // textbox had before to indicate that the shortcut registration was
351 // canceled.
352 if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode))
353 shortcutNode.textContent = this.oldValue_;
355 var commandClear = commandShortcut.querySelector('.command-clear');
356 if (this.oldValue_ == '') {
357 commandShortcut.classList.remove('clearable');
358 commandClear.hidden = true;
359 } else {
360 commandShortcut.classList.add('clearable');
361 commandClear.hidden = false;
364 this.oldValue_ = '';
365 this.capturingElement_ = null;
366 this.currentKeyEvent_ = null;
370 * Handles focus event and adds visual indication for active shortcut.
371 * @param {Event} event to consider.
372 * @private
374 handleFocus_: function(event) {
375 var commandShortcut = event.target.parentElement;
376 commandShortcut.classList.add('focused');
380 * Handles lost focus event and removes visual indication of active shortcut
381 * also stops capturing on focus lost.
382 * @param {Event} event to consider.
383 * @private
385 handleBlur_: function(event) {
386 this.endCapture_(event);
387 var commandShortcut = event.target.parentElement;
388 commandShortcut.classList.remove('focused');
392 * The KeyDown handler.
393 * @param {Event} event The keyboard event to consider.
394 * @private
396 handleKeyDown_: function(event) {
397 event = /** @type {KeyboardEvent} */(event);
398 if (event.keyCode == keyEscape) {
399 // Escape cancels capturing.
400 this.endCapture_(event);
401 var parsed = this.parseElementId_('clear',
402 event.target.parentElement.querySelector('.command-clear').id);
403 chrome.send('setExtensionCommandShortcut',
404 [parsed.extensionId, parsed.commandName, '']);
405 event.preventDefault();
406 event.stopPropagation();
407 return;
409 if (event.keyCode == keyTab) {
410 // Allow tab propagation for keyboard navigation.
411 return;
414 if (!this.capturingElement_)
415 this.startCapture_(event);
417 this.handleKey_(event);
421 * The KeyUp handler.
422 * @param {Event} event The keyboard event to consider.
423 * @private
425 handleKeyUp_: function(event) {
426 event = /** @type {KeyboardEvent} */(event);
427 if (event.keyCode == keyTab) {
428 // Allow tab propagation for keyboard navigation.
429 return;
432 // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by
433 // releasing Shift, but we also don't want it to be easy to lose for
434 // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl
435 // as fast as the other two keys. Therefore, we process KeyUp until you
436 // have a valid combination and then stop processing it (meaning that once
437 // you have a valid combination, we won't change it until the next
438 // KeyDown message arrives).
439 if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode)) {
440 if (!event.ctrlKey && !event.altKey) {
441 // If neither Ctrl nor Alt is pressed then it is not a valid shortcut.
442 // That means we're back at the starting point so we should restart
443 // capture.
444 this.endCapture_(event);
445 this.startCapture_(event);
446 } else {
447 this.handleKey_(event);
453 * A general key handler (used for both KeyDown and KeyUp).
454 * @param {KeyboardEvent} event The keyboard event to consider.
455 * @private
457 handleKey_: function(event) {
458 // While capturing, we prevent all events from bubbling, to prevent
459 // shortcuts lacking the right modifier (F3 for example) from activating
460 // and ending capture prematurely.
461 event.preventDefault();
462 event.stopPropagation();
464 if (modifiers(event.keyCode) == Modifiers.ARE_REQUIRED &&
465 !hasModifier(event, false)) {
466 // Ctrl or Alt (or Cmd on Mac) is a must for most shortcuts.
467 return;
470 if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
471 hasModifier(event, true)) {
472 return;
475 var shortcutNode = this.capturingElement_;
476 var keystroke = keystrokeToString(event);
477 shortcutNode.textContent = keystroke;
478 event.target.classList.add('contains-chars');
479 this.currentKeyEvent_ = event;
481 if (validChar(event.keyCode)) {
482 var node = event.target;
483 while (node && !node.id)
484 node = node.parentElement;
486 this.oldValue_ = keystroke; // Forget what the old value was.
487 var parsed = this.parseElementId_('command', node.id);
489 // Ending the capture must occur before calling
490 // setExtensionCommandShortcut to ensure the shortcut is set.
491 this.endCapture_(event);
492 chrome.send('setExtensionCommandShortcut',
493 [parsed.extensionId, parsed.commandName, keystroke]);
498 * A handler for the delete command button.
499 * @param {Event} event The mouse event to consider.
500 * @private
502 handleClear_: function(event) {
503 var parsed = this.parseElementId_('clear', event.target.id);
504 chrome.send('setExtensionCommandShortcut',
505 [parsed.extensionId, parsed.commandName, '']);
509 * A handler for the setting the scope of the command.
510 * @param {Event} event The mouse event to consider.
511 * @private
513 handleSetCommandScope_: function(event) {
514 var parsed = this.parseElementId_('setCommandScope', event.target.id);
515 var element = document.getElementById(
516 'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName);
517 chrome.send('setCommandScope',
518 [parsed.extensionId, parsed.commandName, element.selectedIndex == 1]);
522 * A utility function to create a unique element id based on a namespace,
523 * extension id and a command name.
524 * @param {string} namespace The namespace to prepend the id with.
525 * @param {string} extensionId The extension ID to use in the id.
526 * @param {string} commandName The command name to append the id with.
527 * @private
529 createElementId_: function(namespace, extensionId, commandName) {
530 return namespace + '-' + extensionId + '-' + commandName;
534 * A utility function to parse a unique element id based on a namespace,
535 * extension id and a command name.
536 * @param {string} namespace The namespace to prepend the id with.
537 * @param {string} id The id to parse.
538 * @return {{extensionId: string, commandName: string}} The parsed id.
539 * @private
541 parseElementId_: function(namespace, id) {
542 var kExtensionIdLength = 32;
543 return {
544 extensionId: id.substring(namespace.length + 1,
545 namespace.length + 1 + kExtensionIdLength),
546 commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
551 return {
552 ExtensionCommandList: ExtensionCommandList