Upstream version 6.35.121.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 cr.define('options', function() {
6   'use strict';
7
8   /**
9    * Creates a new list of extension commands.
10    * @param {Object=} opt_propertyBag Optional properties.
11    * @constructor
12    * @extends {cr.ui.div}
13    */
14   var ExtensionCommandList = cr.ui.define('div');
15
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;
34
35   /**
36    * Enum for whether we require modifiers of a keycode.
37    * @enum {number}
38    */
39   var Modifiers = {
40     ARE_NOT_ALLOWED: 0,
41     ARE_REQUIRED: 1
42   };
43
44   /**
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
47    * the moment.
48    * @param {int} keyCode The keycode to consider.
49    * @return {boolean} Returns whether the char is valid.
50    */
51   function validChar(keyCode) {
52     return keyCode == keyComma ||
53            keyCode == keyDel ||
54            keyCode == keyDown ||
55            keyCode == keyEnd ||
56            keyCode == keyHome ||
57            keyCode == keyIns ||
58            keyCode == keyLeft ||
59            keyCode == keyMediaNextTrack ||
60            keyCode == keyMediaPlayPause ||
61            keyCode == keyMediaPrevTrack ||
62            keyCode == keyMediaStop ||
63            keyCode == keyPageDown ||
64            keyCode == keyPageUp ||
65            keyCode == keyPeriod ||
66            keyCode == keyRight ||
67            keyCode == keyTab ||
68            keyCode == keyUp ||
69            (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
70            (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
71   }
72
73   /**
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.
78    */
79   function keystrokeToString(event) {
80     var output = '';
81     if (cr.isMac && event.metaKey)
82       output = 'Command+';
83     if (event.ctrlKey)
84       output = 'Ctrl+';
85     if (!event.ctrlKey && event.altKey)
86       output += 'Alt+';
87     if (event.shiftKey)
88       output += 'Shift+';
89
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);
95       } else {
96         switch (keyCode) {
97           case keyComma:
98             output += 'Comma'; break;
99           case keyDel:
100             output += 'Delete'; break;
101           case keyDown:
102             output += 'Down'; break;
103           case keyEnd:
104             output += 'End'; break;
105           case keyHome:
106             output += 'Home'; break;
107           case keyIns:
108             output += 'Insert'; break;
109           case keyLeft:
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;
117           case keyMediaStop:
118             output += 'MediaStop'; break;
119           case keyPageDown:
120             output += 'PageDown'; break;
121           case keyPageUp:
122             output += 'PageUp'; break;
123           case keyPeriod:
124             output += 'Period'; break;
125           case keyRight:
126             output += 'Right'; break;
127           case keyTab:
128             output += 'Tab'; break;
129           case keyUp:
130             output += 'Up'; break;
131         }
132       }
133     }
134
135     return output;
136   }
137
138   /**
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.
144    */
145   function modifiers(keyCode) {
146     switch (keyCode) {
147       case keyMediaNextTrack:
148       case keyMediaPlayPause:
149       case keyMediaPrevTrack:
150       case keyMediaStop:
151         return Modifiers.ARE_NOT_ALLOWED;
152       default:
153         return Modifiers.ARE_REQUIRED;
154     }
155   }
156
157   /**
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.
164    */
165   function hasModifier(event, countShiftAsModifier) {
166     return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
167            (countShiftAsModifier && event.shiftKey);
168   }
169
170   ExtensionCommandList.prototype = {
171     __proto__: HTMLDivElement.prototype,
172
173     /**
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}.
178      * @private
179      */
180     currentKeyEvent_: null,
181
182     /**
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.
185      * @type: {string}.
186      * @private
187      */
188     oldValue_: '',
189
190     /**
191      * While capturing, this keeps track of which element the user asked to
192      * change.
193      * @type: {HTMLElement}.
194      * @private
195      */
196     capturingElement_: null,
197
198     /** @override */
199     decorate: function() {
200       this.textContent = '';
201
202       // Iterate over the extension data and add each item to the list.
203       this.data_.commands.forEach(this.createNodeForExtension_.bind(this));
204     },
205
206     /**
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.
210      * @private
211      */
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);
216
217       var title = node.querySelector('.extension-title');
218       title.textContent = extension.name;
219
220       this.appendChild(node);
221
222       // Iterate over the commands data within the extension and add each item
223       // to the list.
224       extension.commands.forEach(this.createNodeForCommand_.bind(this));
225     },
226
227     /**
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.
231      * @private
232      */
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);
239
240       var description = node.querySelector('.command-description');
241       description.textContent = command.description;
242
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');
254
255         var commandShortcut = node.querySelector('.command-shortcut');
256         commandShortcut.classList.add('inactive-keybinding');
257       } else {
258         shortcutNode.textContent = command.keybinding;
259       }
260
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));
266
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;
280         } else {
281           // Add the 'Global' option.
282           option = document.createElement('option');
283           option.textContent =
284               loadTimeData.getString('extensionCommandsGlobal');
285           select.appendChild(option);
286           select.selectedIndex = command.global ? 1 : 0;
287
288           select.addEventListener(
289               'change', this.handleSetCommandScope_.bind(this));
290         }
291       }
292
293       this.appendChild(node);
294     },
295
296     /**
297      * Starts keystroke capture to determine which key to use for a particular
298      * extension command.
299      * @param {Event} event The keyboard event to consider.
300      * @private
301      */
302     startCapture_: function(event) {
303       if (this.capturingElement_)
304         return;  // Already capturing.
305
306       chrome.send('setShortcutHandlingSuspended', [true]);
307
308       var shortcutNode = event.target;
309       this.oldValue_ = shortcutNode.textContent;
310       shortcutNode.textContent =
311           loadTimeData.getString('extensionCommandsStartTyping');
312       shortcutNode.parentElement.classList.add('capturing');
313
314       var commandClear =
315           shortcutNode.parentElement.querySelector('.command-clear');
316       commandClear.hidden = true;
317
318       this.capturingElement_ = event.target;
319     },
320
321     /**
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.
325      * @private
326      */
327     endCapture_: function(event) {
328       if (!this.capturingElement_)
329         return;  // Not capturing.
330
331       chrome.send('setShortcutHandlingSuspended', [false]);
332
333       var shortcutNode = this.capturingElement_;
334       var commandShortcut = shortcutNode.parentElement;
335
336       commandShortcut.classList.remove('capturing');
337       commandShortcut.classList.remove('contains-chars');
338
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
344       // canceled.
345       if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode))
346         shortcutNode.textContent = this.oldValue_;
347
348       var commandClear = commandShortcut.querySelector('.command-clear');
349       if (this.oldValue_ == '') {
350         commandShortcut.classList.remove('clearable');
351         commandClear.hidden = true;
352       } else {
353         commandShortcut.classList.add('clearable');
354         commandClear.hidden = false;
355       }
356
357       this.oldValue_ = '';
358       this.capturingElement_ = null;
359       this.currentKeyEvent_ = null;
360     },
361
362     /**
363      * Handles focus event and adds visual indication for active shortcut.
364      * @param {Event} event to consider.
365      * @private
366      */
367     handleFocus_: function(event) {
368       var commandShortcut = event.target.parentElement;
369       commandShortcut.classList.add('focused');
370     },
371
372     /**
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.
376      * @private
377      */
378     handleBlur_: function(event) {
379       this.endCapture_(event);
380       var commandShortcut = event.target.parentElement;
381       commandShortcut.classList.remove('focused');
382     },
383
384     /**
385      * The KeyDown handler.
386      * @param {Event} event The keyboard event to consider.
387      * @private
388      */
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();
399         return;
400       }
401       if (event.keyCode == keyTab) {
402         // Allow tab propagation for keyboard navigation.
403         return;
404       }
405
406       if (!this.capturingElement_)
407         this.startCapture_(event);
408
409       this.handleKey_(event);
410     },
411
412     /**
413      * The KeyUp handler.
414      * @param {Event} event The keyboard event to consider.
415      * @private
416      */
417     handleKeyUp_: function(event) {
418       if (event.keyCode == keyTab) {
419         // Allow tab propagation for keyboard navigation.
420         return;
421       }
422
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
434           // capture.
435           this.endCapture_(event);
436           this.startCapture_(event);
437         } else {
438           this.handleKey_(event);
439         }
440       }
441     },
442
443     /**
444      * A general key handler (used for both KeyDown and KeyUp).
445      * @param {Event} event The keyboard event to consider.
446      * @private
447      */
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();
454
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.
458         return;
459       }
460
461       if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
462           hasModifier(event, true)) {
463         return;
464       }
465
466       var shortcutNode = this.capturingElement_;
467       var keystroke = keystrokeToString(event);
468       shortcutNode.textContent = keystroke;
469       event.target.classList.add('contains-chars');
470
471       if (validChar(event.keyCode)) {
472         var node = event.target;
473         while (node && !node.id)
474           node = node.parentElement;
475
476         this.oldValue_ = keystroke;  // Forget what the old value was.
477         var parsed = this.parseElementId_('command', node.id);
478
479         // Ending the capture must occur before calling
480         // setExtensionCommandShortcut to ensure the shortcut is set.
481         this.endCapture_(event);
482         chrome.send('setExtensionCommandShortcut',
483                     [parsed.extensionId, parsed.commandName, keystroke]);
484       }
485
486       this.currentKeyEvent_ = event;
487     },
488
489     /**
490      * A handler for the delete command button.
491      * @param {Event} event The mouse event to consider.
492      * @private
493      */
494     handleClear_: function(event) {
495       var parsed = this.parseElementId_('clear', event.target.id);
496       chrome.send('setExtensionCommandShortcut',
497           [parsed.extensionId, parsed.commandName, '']);
498     },
499
500     /**
501      * A handler for the setting the scope of the command.
502      * @param {Event} event The mouse event to consider.
503      * @private
504      */
505     handleSetCommandScope_: function(event) {
506       var parsed = this.parseElementId_('setCommandScope', event.target.id);
507       var element = document.getElementById(
508           'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName);
509       chrome.send('setCommandScope',
510           [parsed.extensionId, parsed.commandName, element.selectedIndex == 1]);
511     },
512
513     /**
514      * A utility function to create a unique element id based on a namespace,
515      * extension id and a command name.
516      * @param {string} namespace   The namespace to prepend the id with.
517      * @param {string} extensionId The extension ID to use in the id.
518      * @param {string} commandName The command name to append the id with.
519      * @private
520      */
521     createElementId_: function(namespace, extensionId, commandName) {
522       return namespace + '-' + extensionId + '-' + commandName;
523     },
524
525     /**
526      * A utility function to parse a unique element id based on a namespace,
527      * extension id and a command name.
528      * @param {string} namespace   The namespace to prepend the id with.
529      * @param {string} id          The id to parse.
530      * @return {object} The parsed id, as an object with two members:
531      *                  extensionID and commandName.
532      * @private
533      */
534     parseElementId_: function(namespace, id) {
535       var kExtensionIdLength = 32;
536       return {
537         extensionId: id.substring(namespace.length + 1,
538                                   namespace.length + 1 + kExtensionIdLength),
539         commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
540       };
541     },
542   };
543
544   return {
545     ExtensionCommandList: ExtensionCommandList
546   };
547 });