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() {
19 * Creates a new list of extension commands.
20 * @param {Object=} opt_propertyBag Optional properties.
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;
46 * Enum for whether we require modifiers of a keycode.
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
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
||
69 keyCode
== keyMediaNextTrack
||
70 keyCode
== keyMediaPlayPause
||
71 keyCode
== keyMediaPrevTrack
||
72 keyCode
== keyMediaStop
||
73 keyCode
== keyPageDown
||
74 keyCode
== keyPageUp
||
75 keyCode
== keyPeriod
||
76 keyCode
== keyRight
||
79 (keyCode
>= 'A'.charCodeAt(0) && keyCode
<= 'Z'.charCodeAt(0)) ||
80 (keyCode
>= '0'.charCodeAt(0) && keyCode
<= '9'.charCodeAt(0));
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
) {
91 if (cr
.isMac
&& event
.metaKey
)
95 if (!event
.ctrlKey
&& event
.altKey
)
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);
108 output
+= 'Comma'; break;
110 output
+= 'Delete'; break;
112 output
+= 'Down'; break;
114 output
+= 'End'; break;
116 output
+= 'Home'; break;
118 output
+= 'Insert'; break;
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;
128 output
+= 'MediaStop'; break;
130 output
+= 'PageDown'; break;
132 output
+= 'PageUp'; break;
134 output
+= 'Period'; break;
136 output
+= 'Right'; break;
138 output
+= 'Tab'; break;
140 output
+= 'Up'; break;
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
) {
157 case keyMediaNextTrack
:
158 case keyMediaPlayPause
:
159 case keyMediaPrevTrack
:
161 return Modifiers
.ARE_NOT_ALLOWED
;
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}.
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.
201 * While capturing, this keeps track of which element the user asked to
203 * @type {HTMLElement}.
206 capturingElement_
: null,
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.
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
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
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');
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;
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
306 * @param {Event} event The keyboard event to consider.
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');
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.
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
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;
360 commandShortcut
.classList
.add('clearable');
361 commandClear
.hidden
= false;
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.
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.
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.
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();
409 if (event
.keyCode
== keyTab
) {
410 // Allow tab propagation for keyboard navigation.
414 if (!this.capturingElement_
)
415 this.startCapture_(event
);
417 this.handleKey_(event
);
422 * @param {Event} event The keyboard event to consider.
425 handleKeyUp_: function(event
) {
426 event
= /** @type {KeyboardEvent} */(event
);
427 if (event
.keyCode
== keyTab
) {
428 // Allow tab propagation for keyboard navigation.
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
444 this.endCapture_(event
);
445 this.startCapture_(event
);
447 this.handleKey_(event
);
453 * A general key handler (used for both KeyDown and KeyUp).
454 * @param {KeyboardEvent} event The keyboard event to consider.
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.
470 if (modifiers(event
.keyCode
) == Modifiers
.ARE_NOT_ALLOWED
&&
471 hasModifier(event
, true)) {
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.
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.
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.
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.
541 parseElementId_: function(namespace, id
) {
542 var kExtensionIdLength
= 32;
544 extensionId
: id
.substring(namespace.length
+ 1,
545 namespace.length
+ 1 + kExtensionIdLength
),
546 commandName
: id
.substring(namespace.length
+ 1 + kExtensionIdLength
+ 1)
552 ExtensionCommandList
: ExtensionCommandList