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