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,
8 * extension_action: boolean,
9 * extension_id: string,
11 * keybinding: string}}
15 cr.define('options', function() {
18 /** @const */ var keyComma = 188;
19 /** @const */ var keyDel = 46;
20 /** @const */ var keyDown = 40;
21 /** @const */ var keyEnd = 35;
22 /** @const */ var keyEscape = 27;
23 /** @const */ var keyHome = 36;
24 /** @const */ var keyIns = 45;
25 /** @const */ var keyLeft = 37;
26 /** @const */ var keyMediaNextTrack = 176;
27 /** @const */ var keyMediaPlayPause = 179;
28 /** @const */ var keyMediaPrevTrack = 177;
29 /** @const */ var keyMediaStop = 178;
30 /** @const */ var keyPageDown = 34;
31 /** @const */ var keyPageUp = 33;
32 /** @const */ var keyPeriod = 190;
33 /** @const */ var keyRight = 39;
34 /** @const */ var keySpace = 32;
35 /** @const */ var keyTab = 9;
36 /** @const */ var keyUp = 38;
39 * Enum for whether we require modifiers of a keycode.
48 * Returns whether the passed in |keyCode| is a valid extension command
49 * char or not. This is restricted to A-Z and 0-9 (ignoring modifiers) at
51 * @param {number} keyCode The keycode to consider.
52 * @return {boolean} Returns whether the char is valid.
54 function validChar(keyCode) {
55 return keyCode == keyComma ||
62 keyCode == keyMediaNextTrack ||
63 keyCode == keyMediaPlayPause ||
64 keyCode == keyMediaPrevTrack ||
65 keyCode == keyMediaStop ||
66 keyCode == keyPageDown ||
67 keyCode == keyPageUp ||
68 keyCode == keyPeriod ||
69 keyCode == keyRight ||
70 keyCode == keySpace ||
73 (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
74 (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
78 * Convert a keystroke event to string form, while taking into account
79 * (ignoring) invalid extension commands.
80 * @param {Event} event The keyboard event to convert.
81 * @return {string} The keystroke as a string.
83 function keystrokeToString(event) {
85 if (cr.isMac && event.metaKey)
86 output.push('Command');
87 if (cr.isChromeOS && event.metaKey)
88 output.push('Search');
91 if (!event.ctrlKey && event.altKey)
96 var keyCode = event.keyCode;
97 if (validChar(keyCode)) {
98 if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
99 (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) {
100 output.push(String.fromCharCode('A'.charCodeAt(0) + keyCode - 65));
104 output.push('Comma'); break;
106 output.push('Delete'); break;
108 output.push('Down'); break;
110 output.push('End'); break;
112 output.push('Home'); break;
114 output.push('Insert'); break;
116 output.push('Left'); break;
117 case keyMediaNextTrack:
118 output.push('MediaNextTrack'); break;
119 case keyMediaPlayPause:
120 output.push('MediaPlayPause'); break;
121 case keyMediaPrevTrack:
122 output.push('MediaPrevTrack'); break;
124 output.push('MediaStop'); break;
126 output.push('PageDown'); break;
128 output.push('PageUp'); break;
130 output.push('Period'); break;
132 output.push('Right'); break;
134 output.push('Space'); break;
136 output.push('Tab'); break;
138 output.push('Up'); break;
143 return output.join('+');
147 * Returns whether the passed in |keyCode| require modifiers. Currently only
148 * "MediaNextTrack", "MediaPrevTrack", "MediaStop", "MediaPlayPause" are
149 * required to be used without any modifier.
150 * @param {number} keyCode The keycode to consider.
151 * @return {Modifiers} Returns whether the keycode require modifiers.
153 function modifiers(keyCode) {
155 case keyMediaNextTrack:
156 case keyMediaPlayPause:
157 case keyMediaPrevTrack:
159 return Modifiers.ARE_NOT_ALLOWED;
161 return Modifiers.ARE_REQUIRED;
166 * Return true if the specified keyboard event has any one of following
167 * modifiers: "Ctrl", "Alt", "Cmd" on Mac, and "Shift" when the
168 * countShiftAsModifier is true.
169 * @param {Event} event The keyboard event to consider.
170 * @param {boolean} countShiftAsModifier Whether the 'ShiftKey' should be
171 * counted as modifier.
173 function hasModifier(event, countShiftAsModifier) {
174 return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
175 (cr.isChromeOS && event.metaKey) ||
176 (countShiftAsModifier && event.shiftKey);
180 * Creates a new list of extension commands.
181 * @param {HTMLDivElement} div
183 * @extends {HTMLDivElement}
185 function ExtensionCommandList(div) {
186 div.__proto__ = ExtensionCommandList.prototype;
190 ExtensionCommandList.prototype = {
191 __proto__: HTMLDivElement.prototype,
194 * While capturing, this records the current (last) keyboard event generated
195 * by the user. Will be |null| after capture and during capture when no
196 * keyboard event has been generated.
197 * @type {KeyboardEvent}.
200 currentKeyEvent_: null,
203 * While capturing, this keeps track of the previous selection so we can
204 * revert back to if no valid assignment is made during capture.
211 * While capturing, this keeps track of which element the user asked to
213 * @type {HTMLElement}.
216 capturingElement_: null,
219 * Updates the extensions data for the overlay.
220 * @param {!Array<ExtensionInfo>} data The extension data.
222 setData: function(data) {
223 /** @private {!Array<ExtensionInfo>} */
226 this.textContent = '';
228 // Iterate over the extension data and add each item to the list.
229 this.data_.forEach(this.createNodeForExtension_.bind(this));
233 * Synthesizes and initializes an HTML element for the extension command
234 * metadata given in |extension|.
235 * @param {ExtensionInfo} extension A dictionary of extension metadata.
238 createNodeForExtension_: function(extension) {
239 if (extension.commands.length == 0 ||
240 extension.state == chrome.developerPrivate.ExtensionState.DISABLED)
243 var template = $('template-collection-extension-commands').querySelector(
244 '.extension-command-list-extension-item-wrapper');
245 var node = template.cloneNode(true);
247 var title = node.querySelector('.extension-title');
248 title.textContent = extension.name;
250 this.appendChild(node);
252 // Iterate over the commands data within the extension and add each item
254 extension.commands.forEach(
255 this.createNodeForCommand_.bind(this, extension.id));
259 * Synthesizes and initializes an HTML element for the extension command
260 * metadata given in |command|.
261 * @param {string} extensionId The associated extension's id.
262 * @param {Command} command A dictionary of extension command metadata.
265 createNodeForCommand_: function(extensionId, command) {
266 var template = $('template-collection-extension-commands').querySelector(
267 '.extension-command-list-command-item-wrapper');
268 var node = template.cloneNode(true);
269 node.id = this.createElementId_('command', extensionId, command.name);
271 var description = node.querySelector('.command-description');
272 description.textContent = command.description;
274 var shortcutNode = node.querySelector('.command-shortcut-text');
275 shortcutNode.addEventListener('mouseup',
276 this.startCapture_.bind(this));
277 shortcutNode.addEventListener('focus', this.handleFocus_.bind(this));
278 shortcutNode.addEventListener('blur', this.handleBlur_.bind(this));
279 shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this));
280 shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this));
281 if (!command.isActive) {
282 shortcutNode.textContent =
283 loadTimeData.getString('extensionCommandsInactive');
285 var commandShortcut = node.querySelector('.command-shortcut');
286 commandShortcut.classList.add('inactive-keybinding');
288 shortcutNode.textContent = command.keybinding;
291 var commandClear = node.querySelector('.command-clear');
292 commandClear.id = this.createElementId_(
293 'clear', extensionId, command.name);
294 commandClear.title = loadTimeData.getString('extensionCommandsDelete');
295 commandClear.addEventListener('click', this.handleClear_.bind(this));
297 var select = node.querySelector('.command-scope');
298 select.id = this.createElementId_(
299 'setCommandScope', extensionId, command.name);
300 select.hidden = false;
301 // Add the 'In Chrome' option.
302 var option = document.createElement('option');
303 option.textContent = loadTimeData.getString('extensionCommandsRegular');
304 select.appendChild(option);
305 if (command.isExtensionAction || !command.isActive) {
306 // Extension actions cannot be global, so we might as well disable the
307 // combo box, to signify that, and if the command is inactive, it
308 // doesn't make sense to allow the user to adjust the scope.
309 select.disabled = true;
311 // Add the 'Global' option.
312 option = document.createElement('option');
313 option.textContent = loadTimeData.getString('extensionCommandsGlobal');
314 select.appendChild(option);
315 select.selectedIndex =
316 command.scope == chrome.developerPrivate.CommandScope.GLOBAL ?
319 select.addEventListener(
320 'change', this.handleSetCommandScope_.bind(this));
323 this.appendChild(node);
327 * Starts keystroke capture to determine which key to use for a particular
329 * @param {Event} event The keyboard event to consider.
332 startCapture_: function(event) {
333 if (this.capturingElement_)
334 return; // Already capturing.
336 chrome.developerPrivate.setShortcutHandlingSuspended(true);
338 var shortcutNode = event.target;
339 this.oldValue_ = shortcutNode.textContent;
340 shortcutNode.textContent =
341 loadTimeData.getString('extensionCommandsStartTyping');
342 shortcutNode.parentElement.classList.add('capturing');
345 shortcutNode.parentElement.querySelector('.command-clear');
346 commandClear.hidden = true;
348 this.capturingElement_ = /** @type {HTMLElement} */(event.target);
352 * Ends keystroke capture and either restores the old value or (if valid
353 * value) sets the new value as active..
354 * @param {Event} event The keyboard event to consider.
357 endCapture_: function(event) {
358 if (!this.capturingElement_)
359 return; // Not capturing.
361 chrome.developerPrivate.setShortcutHandlingSuspended(false);
363 var shortcutNode = this.capturingElement_;
364 var commandShortcut = shortcutNode.parentElement;
366 commandShortcut.classList.remove('capturing');
367 commandShortcut.classList.remove('contains-chars');
369 // When the capture ends, the user may have not given a complete and valid
370 // input (or even no input at all). Only a valid key event followed by a
371 // valid key combination will cause a shortcut selection to be activated.
372 // If no valid selection was made, however, revert back to what the
373 // textbox had before to indicate that the shortcut registration was
375 if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode))
376 shortcutNode.textContent = this.oldValue_;
378 var commandClear = commandShortcut.querySelector('.command-clear');
379 if (this.oldValue_ == '') {
380 commandShortcut.classList.remove('clearable');
381 commandClear.hidden = true;
383 commandShortcut.classList.add('clearable');
384 commandClear.hidden = false;
388 this.capturingElement_ = null;
389 this.currentKeyEvent_ = null;
393 * Handles focus event and adds visual indication for active shortcut.
394 * @param {Event} event to consider.
397 handleFocus_: function(event) {
398 var commandShortcut = event.target.parentElement;
399 commandShortcut.classList.add('focused');
403 * Handles lost focus event and removes visual indication of active shortcut
404 * also stops capturing on focus lost.
405 * @param {Event} event to consider.
408 handleBlur_: function(event) {
409 this.endCapture_(event);
410 var commandShortcut = event.target.parentElement;
411 commandShortcut.classList.remove('focused');
415 * The KeyDown handler.
416 * @param {Event} event The keyboard event to consider.
419 handleKeyDown_: function(event) {
420 event = /** @type {KeyboardEvent} */(event);
421 if (event.keyCode == keyEscape) {
422 if (!this.capturingElement_) {
423 // If we're not currently capturing, allow escape to propagate (so it
424 // can close the overflow).
427 // Otherwise, escape cancels capturing.
428 this.endCapture_(event);
429 var parsed = this.parseElementId_('clear',
430 event.target.parentElement.querySelector('.command-clear').id);
431 chrome.developerPrivate.updateExtensionCommand({
432 extensionId: parsed.extensionId,
433 commandName: parsed.commandName,
436 event.preventDefault();
437 event.stopPropagation();
440 if (event.keyCode == keyTab) {
441 // Allow tab propagation for keyboard navigation.
445 if (!this.capturingElement_)
446 this.startCapture_(event);
448 this.handleKey_(event);
453 * @param {Event} event The keyboard event to consider.
456 handleKeyUp_: function(event) {
457 event = /** @type {KeyboardEvent} */(event);
458 if (event.keyCode == keyTab || event.keyCode == keyEscape) {
459 // We need to allow tab propagation for keyboard navigation, and escapes
460 // are fully handled in handleKeyDown.
464 // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by
465 // releasing Shift, but we also don't want it to be easy to lose for
466 // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl
467 // as fast as the other two keys. Therefore, we process KeyUp until you
468 // have a valid combination and then stop processing it (meaning that once
469 // you have a valid combination, we won't change it until the next
470 // KeyDown message arrives).
471 if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode)) {
472 if (!event.ctrlKey && !event.altKey ||
473 ((cr.isMac || cr.isChromeOS) && !event.metaKey)) {
474 // If neither Ctrl nor Alt is pressed then it is not a valid shortcut.
475 // That means we're back at the starting point so we should restart
477 this.endCapture_(event);
478 this.startCapture_(event);
480 this.handleKey_(event);
486 * A general key handler (used for both KeyDown and KeyUp).
487 * @param {KeyboardEvent} event The keyboard event to consider.
490 handleKey_: function(event) {
491 // While capturing, we prevent all events from bubbling, to prevent
492 // shortcuts lacking the right modifier (F3 for example) from activating
493 // and ending capture prematurely.
494 event.preventDefault();
495 event.stopPropagation();
497 if (modifiers(event.keyCode) == Modifiers.ARE_REQUIRED &&
498 !hasModifier(event, false)) {
499 // Ctrl or Alt (or Cmd on Mac) is a must for most shortcuts.
503 if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
504 hasModifier(event, true)) {
508 var shortcutNode = this.capturingElement_;
509 var keystroke = keystrokeToString(event);
510 shortcutNode.textContent = keystroke;
511 event.target.classList.add('contains-chars');
512 this.currentKeyEvent_ = event;
514 if (validChar(event.keyCode)) {
515 var node = event.target;
516 while (node && !node.id)
517 node = node.parentElement;
519 this.oldValue_ = keystroke; // Forget what the old value was.
520 var parsed = this.parseElementId_('command', node.id);
522 // Ending the capture must occur before calling
523 // setExtensionCommandShortcut to ensure the shortcut is set.
524 this.endCapture_(event);
525 chrome.developerPrivate.updateExtensionCommand(
526 {extensionId: parsed.extensionId,
527 commandName: parsed.commandName,
528 keybinding: keystroke});
533 * A handler for the delete command button.
534 * @param {Event} event The mouse event to consider.
537 handleClear_: function(event) {
538 var parsed = this.parseElementId_('clear', event.target.id);
539 chrome.developerPrivate.updateExtensionCommand(
540 {extensionId: parsed.extensionId,
541 commandName: parsed.commandName,
546 * A handler for the setting the scope of the command.
547 * @param {Event} event The mouse event to consider.
550 handleSetCommandScope_: function(event) {
551 var parsed = this.parseElementId_('setCommandScope', event.target.id);
552 var element = document.getElementById(
553 'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName);
554 var scope = element.selectedIndex == 1 ?
555 chrome.developerPrivate.CommandScope.GLOBAL :
556 chrome.developerPrivate.CommandScope.CHROME;
557 chrome.developerPrivate.updateExtensionCommand(
558 {extensionId: parsed.extensionId,
559 commandName: parsed.commandName,
564 * A utility function to create a unique element id based on a namespace,
565 * extension id and a command name.
566 * @param {string} namespace The namespace to prepend the id with.
567 * @param {string} extensionId The extension ID to use in the id.
568 * @param {string} commandName The command name to append the id with.
571 createElementId_: function(namespace, extensionId, commandName) {
572 return namespace + '-' + extensionId + '-' + commandName;
576 * A utility function to parse a unique element id based on a namespace,
577 * extension id and a command name.
578 * @param {string} namespace The namespace to prepend the id with.
579 * @param {string} id The id to parse.
580 * @return {{extensionId: string, commandName: string}} The parsed id.
583 parseElementId_: function(namespace, id) {
584 var kExtensionIdLength = 32;
586 extensionId: id.substring(namespace.length + 1,
587 namespace.length + 1 + kExtensionIdLength),
588 commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
594 ExtensionCommandList: ExtensionCommandList