Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / extensions / extension_command_list.js
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.
4
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}}
12  */
13 var ExtensionCommand;
14
15 cr.define('options', function() {
16   'use strict';
17
18   /**
19    * Creates a new list of extension commands.
20    * @param {Object=} opt_propertyBag Optional properties.
21    * @constructor
22    * @extends {HTMLDivElement}
23    */
24   var ExtensionCommandList = cr.ui.define('div');
25
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;
44
45   /**
46    * Enum for whether we require modifiers of a keycode.
47    * @enum {number}
48    */
49   var Modifiers = {
50     ARE_NOT_ALLOWED: 0,
51     ARE_REQUIRED: 1
52   };
53
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.
60    */
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));
81   }
82
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.
88    */
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+';
99
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;
141         }
142       }
143     }
144
145     return output;
146   }
147
148   /**
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.
154    */
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;
164     }
165   }
166
167   /**
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.
174    */
175   function hasModifier(event, countShiftAsModifier) {
176     return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
177            (countShiftAsModifier && event.shiftKey);
178   }
179
180   ExtensionCommandList.prototype = {
181     __proto__: HTMLDivElement.prototype,
182
183     /**
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
189      */
190     currentKeyEvent_: null,
191
192     /**
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
197      */
198     oldValue_: '',
199
200     /**
201      * While capturing, this keeps track of which element the user asked to
202      * change.
203      * @type {HTMLElement}.
204      * @private
205      */
206     capturingElement_: null,
207
208     /** @override */
209     decorate: function() {
210       this.textContent = '';
211
212       // Iterate over the extension data and add each item to the list.
213       this.data_.commands.forEach(this.createNodeForExtension_.bind(this));
214     },
215
216     /**
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
221      */
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);
226
227       var title = node.querySelector('.extension-title');
228       title.textContent = extension.name;
229
230       this.appendChild(node);
231
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));
235     },
236
237     /**
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
243      */
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);
250
251       var description = node.querySelector('.command-description');
252       description.textContent = command.description;
253
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');
264
265         var commandShortcut = node.querySelector('.command-shortcut');
266         commandShortcut.classList.add('inactive-keybinding');
267       } else {
268         shortcutNode.textContent = command.keybinding;
269       }
270
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));
276
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;
295
296         select.addEventListener(
297             'change', this.handleSetCommandScope_.bind(this));
298       }
299
300       this.appendChild(node);
301     },
302
303     /**
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
308      */
309     startCapture_: function(event) {
310       if (this.capturingElement_)
311         return;  // Already capturing.
312
313       chrome.send('setShortcutHandlingSuspended', [true]);
314
315       var shortcutNode = event.target;
316       this.oldValue_ = shortcutNode.textContent;
317       shortcutNode.textContent =
318           loadTimeData.getString('extensionCommandsStartTyping');
319       shortcutNode.parentElement.classList.add('capturing');
320
321       var commandClear =
322           shortcutNode.parentElement.querySelector('.command-clear');
323       commandClear.hidden = true;
324
325       this.capturingElement_ = /** @type {HTMLElement} */(event.target);
326     },
327
328     /**
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
333      */
334     endCapture_: function(event) {
335       if (!this.capturingElement_)
336         return;  // Not capturing.
337
338       chrome.send('setShortcutHandlingSuspended', [false]);
339
340       var shortcutNode = this.capturingElement_;
341       var commandShortcut = shortcutNode.parentElement;
342
343       commandShortcut.classList.remove('capturing');
344       commandShortcut.classList.remove('contains-chars');
345
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_;
354
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;
362       }
363
364       this.oldValue_ = '';
365       this.capturingElement_ = null;
366       this.currentKeyEvent_ = null;
367     },
368
369     /**
370      * Handles focus event and adds visual indication for active shortcut.
371      * @param {Event} event to consider.
372      * @private
373      */
374     handleFocus_: function(event) {
375       var commandShortcut = event.target.parentElement;
376       commandShortcut.classList.add('focused');
377     },
378
379     /**
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
384      */
385     handleBlur_: function(event) {
386       this.endCapture_(event);
387       var commandShortcut = event.target.parentElement;
388       commandShortcut.classList.remove('focused');
389     },
390
391     /**
392      * The KeyDown handler.
393      * @param {Event} event The keyboard event to consider.
394      * @private
395      */
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;
408       }
409       if (event.keyCode == keyTab) {
410         // Allow tab propagation for keyboard navigation.
411         return;
412       }
413
414       if (!this.capturingElement_)
415         this.startCapture_(event);
416
417       this.handleKey_(event);
418     },
419
420     /**
421      * The KeyUp handler.
422      * @param {Event} event The keyboard event to consider.
423      * @private
424      */
425     handleKeyUp_: function(event) {
426       event = /** @type {KeyboardEvent} */(event);
427       if (event.keyCode == keyTab) {
428         // Allow tab propagation for keyboard navigation.
429         return;
430       }
431
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);
448         }
449       }
450     },
451
452     /**
453      * A general key handler (used for both KeyDown and KeyUp).
454      * @param {KeyboardEvent} event The keyboard event to consider.
455      * @private
456      */
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();
463
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;
468       }
469
470       if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
471           hasModifier(event, true)) {
472         return;
473       }
474
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;
480
481       if (validChar(event.keyCode)) {
482         var node = event.target;
483         while (node && !node.id)
484           node = node.parentElement;
485
486         this.oldValue_ = keystroke;  // Forget what the old value was.
487         var parsed = this.parseElementId_('command', node.id);
488
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]);
494       }
495     },
496
497     /**
498      * A handler for the delete command button.
499      * @param {Event} event The mouse event to consider.
500      * @private
501      */
502     handleClear_: function(event) {
503       var parsed = this.parseElementId_('clear', event.target.id);
504       chrome.send('setExtensionCommandShortcut',
505           [parsed.extensionId, parsed.commandName, '']);
506     },
507
508     /**
509      * A handler for the setting the scope of the command.
510      * @param {Event} event The mouse event to consider.
511      * @private
512      */
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]);
519     },
520
521     /**
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
528      */
529     createElementId_: function(namespace, extensionId, commandName) {
530       return namespace + '-' + extensionId + '-' + commandName;
531     },
532
533     /**
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
540      */
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)
547       };
548     },
549   };
550
551   return {
552     ExtensionCommandList: ExtensionCommandList
553   };
554 });