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 cr
.define('options', function() {
9 * Creates a new list of extension commands.
10 * @param {Object=} opt_propertyBag Optional properties.
12 * @extends {cr.ui.div}
14 var ExtensionCommandList
= cr
.ui
.define('div');
16 /** @const */ var keyComma
= 188;
17 /** @const */ var keyDel
= 46;
18 /** @const */ var keyDown
= 40;
19 /** @const */ var keyEnd
= 35;
20 /** @const */ var keyEscape
= 27;
21 /** @const */ var keyHome
= 36;
22 /** @const */ var keyIns
= 45;
23 /** @const */ var keyLeft
= 37;
24 /** @const */ var keyMediaNextTrack
= 176;
25 /** @const */ var keyMediaPlayPause
= 179;
26 /** @const */ var keyMediaPrevTrack
= 177;
27 /** @const */ var keyMediaStop
= 178;
28 /** @const */ var keyPageDown
= 34;
29 /** @const */ var keyPageUp
= 33;
30 /** @const */ var keyPeriod
= 190;
31 /** @const */ var keyRight
= 39;
32 /** @const */ var keyTab
= 9;
33 /** @const */ var keyUp
= 38;
36 * Enum for whether we require modifiers of a keycode.
45 * Returns whether the passed in |keyCode| is a valid extension command
46 * char or not. This is restricted to A-Z and 0-9 (ignoring modifiers) at
48 * @param {int} keyCode The keycode to consider.
49 * @return {boolean} Returns whether the char is valid.
51 function validChar(keyCode
) {
52 return keyCode
== keyComma
||
59 keyCode
== keyMediaNextTrack
||
60 keyCode
== keyMediaPlayPause
||
61 keyCode
== keyMediaPrevTrack
||
62 keyCode
== keyMediaStop
||
63 keyCode
== keyPageDown
||
64 keyCode
== keyPageUp
||
65 keyCode
== keyPeriod
||
66 keyCode
== keyRight
||
69 (keyCode
>= 'A'.charCodeAt(0) && keyCode
<= 'Z'.charCodeAt(0)) ||
70 (keyCode
>= '0'.charCodeAt(0) && keyCode
<= '9'.charCodeAt(0));
74 * Convert a keystroke event to string form, while taking into account
75 * (ignoring) invalid extension commands.
76 * @param {Event} event The keyboard event to convert.
77 * @return {string} The keystroke as a string.
79 function keystrokeToString(event
) {
81 if (cr
.isMac
&& event
.metaKey
)
85 if (!event
.ctrlKey
&& event
.altKey
)
90 var keyCode
= event
.keyCode
;
91 if (validChar(keyCode
)) {
92 if ((keyCode
>= 'A'.charCodeAt(0) && keyCode
<= 'Z'.charCodeAt(0)) ||
93 (keyCode
>= '0'.charCodeAt(0) && keyCode
<= '9'.charCodeAt(0))) {
94 output
+= String
.fromCharCode('A'.charCodeAt(0) + keyCode
- 65);
98 output
+= 'Comma'; break;
100 output
+= 'Delete'; break;
102 output
+= 'Down'; break;
104 output
+= 'End'; break;
106 output
+= 'Home'; break;
108 output
+= 'Insert'; break;
110 output
+= 'Left'; break;
111 case keyMediaNextTrack
:
112 output
+= 'MediaNextTrack'; break;
113 case keyMediaPlayPause
:
114 output
+= 'MediaPlayPause'; break;
115 case keyMediaPrevTrack
:
116 output
+= 'MediaPrevTrack'; break;
118 output
+= 'MediaStop'; break;
120 output
+= 'PageDown'; break;
122 output
+= 'PageUp'; break;
124 output
+= 'Period'; break;
126 output
+= 'Right'; break;
128 output
+= 'Tab'; break;
130 output
+= 'Up'; break;
139 * Returns whether the passed in |keyCode| require modifiers. Currently only
140 * "MediaNextTrack", "MediaPrevTrack", "MediaStop", "MediaPlayPause" are
141 * required to be used without any modifier.
142 * @param {int} keyCode The keycode to consider.
143 * @return {Modifiers} Returns whether the keycode require modifiers.
145 function modifiers(keyCode
) {
147 case keyMediaNextTrack
:
148 case keyMediaPlayPause
:
149 case keyMediaPrevTrack
:
151 return Modifiers
.ARE_NOT_ALLOWED
;
153 return Modifiers
.ARE_REQUIRED
;
158 * Return true if the specified keyboard event has any one of following
159 * modifiers: "Ctrl", "Alt", "Cmd" on Mac, and "Shift" when the
160 * countShiftAsModifier is true.
161 * @param {Event} event The keyboard event to consider.
162 * @param {boolean} countShiftAsModifier Whether the 'ShiftKey' should be
163 * counted as modifier.
165 function hasModifier(event
, countShiftAsModifier
) {
166 return event
.ctrlKey
|| event
.altKey
|| (cr
.isMac
&& event
.metaKey
) ||
167 (countShiftAsModifier
&& event
.shiftKey
);
170 ExtensionCommandList
.prototype = {
171 __proto__
: HTMLDivElement
.prototype,
174 * While capturing, this records the current (last) keyboard event generated
175 * by the user. Will be |null| after capture and during capture when no
176 * keyboard event has been generated.
177 * @type: {keyboard event}.
180 currentKeyEvent_
: null,
183 * While capturing, this keeps track of the previous selection so we can
184 * revert back to if no valid assignment is made during capture.
191 * While capturing, this keeps track of which element the user asked to
193 * @type: {HTMLElement}.
196 capturingElement_
: null,
199 decorate: function() {
200 this.textContent
= '';
202 // Iterate over the extension data and add each item to the list.
203 this.data_
.commands
.forEach(this.createNodeForExtension_
.bind(this));
207 * Synthesizes and initializes an HTML element for the extension command
208 * metadata given in |extension|.
209 * @param {Object} extension A dictionary of extension metadata.
212 createNodeForExtension_: function(extension
) {
213 var template
= $('template-collection-extension-commands').querySelector(
214 '.extension-command-list-extension-item-wrapper');
215 var node
= template
.cloneNode(true);
217 var title
= node
.querySelector('.extension-title');
218 title
.textContent
= extension
.name
;
220 this.appendChild(node
);
222 // Iterate over the commands data within the extension and add each item
224 extension
.commands
.forEach(this.createNodeForCommand_
.bind(this));
228 * Synthesizes and initializes an HTML element for the extension command
229 * metadata given in |command|.
230 * @param {Object} command A dictionary of extension command metadata.
233 createNodeForCommand_: function(command
) {
234 var template
= $('template-collection-extension-commands').querySelector(
235 '.extension-command-list-command-item-wrapper');
236 var node
= template
.cloneNode(true);
237 node
.id
= this.createElementId_(
238 'command', command
.extension_id
, command
.command_name
);
240 var description
= node
.querySelector('.command-description');
241 description
.textContent
= command
.description
;
243 var shortcutNode
= node
.querySelector('.command-shortcut-text');
244 shortcutNode
.addEventListener('mouseup',
245 this.startCapture_
.bind(this));
246 shortcutNode
.addEventListener('focus', this.handleFocus_
.bind(this));
247 shortcutNode
.addEventListener('blur', this.handleBlur_
.bind(this));
248 shortcutNode
.addEventListener('keydown',
249 this.handleKeyDown_
.bind(this));
250 shortcutNode
.addEventListener('keyup', this.handleKeyUp_
.bind(this));
251 if (!command
.active
) {
252 shortcutNode
.textContent
=
253 loadTimeData
.getString('extensionCommandsInactive');
255 var commandShortcut
= node
.querySelector('.command-shortcut');
256 commandShortcut
.classList
.add('inactive-keybinding');
258 shortcutNode
.textContent
= command
.keybinding
;
261 var commandClear
= node
.querySelector('.command-clear');
262 commandClear
.id
= this.createElementId_(
263 'clear', command
.extension_id
, command
.command_name
);
264 commandClear
.title
= loadTimeData
.getString('extensionCommandsDelete');
265 commandClear
.addEventListener('click', this.handleClear_
.bind(this));
267 if (command
.scope_ui_visible
) {
268 var select
= node
.querySelector('.command-scope');
269 select
.id
= this.createElementId_(
270 'setCommandScope', command
.extension_id
, command
.command_name
);
271 select
.hidden
= false;
272 // Add the 'In Chrome' option.
273 var option
= document
.createElement('option');
274 option
.textContent
= loadTimeData
.getString('extensionCommandsRegular');
275 select
.appendChild(option
);
276 if (command
.extension_action
) {
277 // Extension actions cannot be global, so we might as well disable the
278 // combo box, to signify that.
279 select
.disabled
= true;
281 // Add the 'Global' option.
282 option
= document
.createElement('option');
284 loadTimeData
.getString('extensionCommandsGlobal');
285 select
.appendChild(option
);
286 select
.selectedIndex
= command
.global
? 1 : 0;
288 select
.addEventListener(
289 'change', this.handleSetCommandScope_
.bind(this));
293 this.appendChild(node
);
297 * Starts keystroke capture to determine which key to use for a particular
299 * @param {Event} event The keyboard event to consider.
302 startCapture_: function(event
) {
303 if (this.capturingElement_
)
304 return; // Already capturing.
306 chrome
.send('setShortcutHandlingSuspended', [true]);
308 var shortcutNode
= event
.target
;
309 this.oldValue_
= shortcutNode
.textContent
;
310 shortcutNode
.textContent
=
311 loadTimeData
.getString('extensionCommandsStartTyping');
312 shortcutNode
.parentElement
.classList
.add('capturing');
315 shortcutNode
.parentElement
.querySelector('.command-clear');
316 commandClear
.hidden
= true;
318 this.capturingElement_
= event
.target
;
322 * Ends keystroke capture and either restores the old value or (if valid
323 * value) sets the new value as active..
324 * @param {Event} event The keyboard event to consider.
327 endCapture_: function(event
) {
328 if (!this.capturingElement_
)
329 return; // Not capturing.
331 chrome
.send('setShortcutHandlingSuspended', [false]);
333 var shortcutNode
= this.capturingElement_
;
334 var commandShortcut
= shortcutNode
.parentElement
;
336 commandShortcut
.classList
.remove('capturing');
337 commandShortcut
.classList
.remove('contains-chars');
339 // When the capture ends, the user may have not given a complete and valid
340 // input (or even no input at all). Only a valid key event followed by a
341 // valid key combination will cause a shortcut selection to be activated.
342 // If no valid selection was made, however, revert back to what the
343 // textbox had before to indicate that the shortcut registration was
345 if (!this.currentKeyEvent_
|| !validChar(this.currentKeyEvent_
.keyCode
))
346 shortcutNode
.textContent
= this.oldValue_
;
348 var commandClear
= commandShortcut
.querySelector('.command-clear');
349 if (this.oldValue_
== '') {
350 commandShortcut
.classList
.remove('clearable');
351 commandClear
.hidden
= true;
353 commandShortcut
.classList
.add('clearable');
354 commandClear
.hidden
= false;
358 this.capturingElement_
= null;
359 this.currentKeyEvent_
= null;
363 * Handles focus event and adds visual indication for active shortcut.
364 * @param {Event} event to consider.
367 handleFocus_: function(event
) {
368 var commandShortcut
= event
.target
.parentElement
;
369 commandShortcut
.classList
.add('focused');
373 * Handles lost focus event and removes visual indication of active shortcut
374 * also stops capturing on focus lost.
375 * @param {Event} event to consider.
378 handleBlur_: function(event
) {
379 this.endCapture_(event
);
380 var commandShortcut
= event
.target
.parentElement
;
381 commandShortcut
.classList
.remove('focused');
385 * The KeyDown handler.
386 * @param {Event} event The keyboard event to consider.
389 handleKeyDown_: function(event
) {
390 if (event
.keyCode
== keyEscape
) {
391 // Escape cancels capturing.
392 this.endCapture_(event
);
393 var parsed
= this.parseElementId_('clear',
394 event
.target
.parentElement
.querySelector('.command-clear').id
);
395 chrome
.send('setExtensionCommandShortcut',
396 [parsed
.extensionId
, parsed
.commandName
, '']);
397 event
.preventDefault();
398 event
.stopPropagation();
401 if (event
.keyCode
== keyTab
) {
402 // Allow tab propagation for keyboard navigation.
406 if (!this.capturingElement_
)
407 this.startCapture_(event
);
409 this.handleKey_(event
);
414 * @param {Event} event The keyboard event to consider.
417 handleKeyUp_: function(event
) {
418 if (event
.keyCode
== keyTab
) {
419 // Allow tab propagation for keyboard navigation.
423 // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by
424 // releasing Shift, but we also don't want it to be easy to lose for
425 // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl
426 // as fast as the other two keys. Therefore, we process KeyUp until you
427 // have a valid combination and then stop processing it (meaning that once
428 // you have a valid combination, we won't change it until the next
429 // KeyDown message arrives).
430 if (!this.currentKeyEvent_
|| !validChar(this.currentKeyEvent_
.keyCode
)) {
431 if (!event
.ctrlKey
&& !event
.altKey
) {
432 // If neither Ctrl nor Alt is pressed then it is not a valid shortcut.
433 // That means we're back at the starting point so we should restart
435 this.endCapture_(event
);
436 this.startCapture_(event
);
438 this.handleKey_(event
);
444 * A general key handler (used for both KeyDown and KeyUp).
445 * @param {Event} event The keyboard event to consider.
448 handleKey_: function(event
) {
449 // While capturing, we prevent all events from bubbling, to prevent
450 // shortcuts lacking the right modifier (F3 for example) from activating
451 // and ending capture prematurely.
452 event
.preventDefault();
453 event
.stopPropagation();
455 if (modifiers(event
.keyCode
) == Modifiers
.ARE_REQUIRED
&&
456 !hasModifier(event
, false)) {
457 // Ctrl or Alt (or Cmd on Mac) is a must for most shortcuts.
461 if (modifiers(event
.keyCode
) == Modifiers
.ARE_NOT_ALLOWED
&&
462 hasModifier(event
, true)) {
466 var shortcutNode
= this.capturingElement_
;
467 var keystroke
= keystrokeToString(event
);
468 shortcutNode
.textContent
= keystroke
;
469 event
.target
.classList
.add('contains-chars');
471 if (validChar(event
.keyCode
)) {
472 var node
= event
.target
;
473 while (node
&& !node
.id
)
474 node
= node
.parentElement
;
476 this.oldValue_
= keystroke
; // Forget what the old value was.
477 var parsed
= this.parseElementId_('command', node
.id
);
478 chrome
.send('setExtensionCommandShortcut',
479 [parsed
.extensionId
, parsed
.commandName
, keystroke
]);
480 this.endCapture_(event
);
483 this.currentKeyEvent_
= event
;
487 * A handler for the delete command button.
488 * @param {Event} event The mouse event to consider.
491 handleClear_: function(event
) {
492 var parsed
= this.parseElementId_('clear', event
.target
.id
);
493 chrome
.send('setExtensionCommandShortcut',
494 [parsed
.extensionId
, parsed
.commandName
, '']);
498 * A handler for the setting the scope of the command.
499 * @param {Event} event The mouse event to consider.
502 handleSetCommandScope_: function(event
) {
503 var parsed
= this.parseElementId_('setCommandScope', event
.target
.id
);
504 var element
= document
.getElementById(
505 'setCommandScope-' + parsed
.extensionId
+ '-' + parsed
.commandName
);
506 chrome
.send('setCommandScope',
507 [parsed
.extensionId
, parsed
.commandName
, element
.selectedIndex
== 1]);
511 * A utility function to create a unique element id based on a namespace,
512 * extension id and a command name.
513 * @param {string} namespace The namespace to prepend the id with.
514 * @param {string} extensionId The extension ID to use in the id.
515 * @param {string} commandName The command name to append the id with.
518 createElementId_: function(namespace, extensionId
, commandName
) {
519 return namespace + '-' + extensionId
+ '-' + commandName
;
523 * A utility function to parse a unique element id based on a namespace,
524 * extension id and a command name.
525 * @param {string} namespace The namespace to prepend the id with.
526 * @param {string} id The id to parse.
527 * @return {object} The parsed id, as an object with two members:
528 * extensionID and commandName.
531 parseElementId_: function(namespace, id
) {
532 var kExtensionIdLength
= 32;
534 extensionId
: id
.substring(namespace.length
+ 1,
535 namespace.length
+ 1 + kExtensionIdLength
),
536 commandName
: id
.substring(namespace.length
+ 1 + kExtensionIdLength
+ 1)
542 ExtensionCommandList
: ExtensionCommandList