Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_command_list.js
blob9974fb72da9beccca7c6acf178ebd3a5af270f3b
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,
7  *             description: string,
8  *             extension_action: boolean,
9  *             extension_id: string,
10  *             global: boolean,
11  *             keybinding: string}}
12  */
13 var ExtensionCommand;
15 cr.define('options', function() {
16   'use strict';
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;
38   /**
39    * Enum for whether we require modifiers of a keycode.
40    * @enum {number}
41    */
42   var Modifiers = {
43     ARE_NOT_ALLOWED: 0,
44     ARE_REQUIRED: 1
45   };
47   /**
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
50    * the moment.
51    * @param {number} keyCode The keycode to consider.
52    * @return {boolean} Returns whether the char is valid.
53    */
54   function validChar(keyCode) {
55     return keyCode == keyComma ||
56            keyCode == keyDel ||
57            keyCode == keyDown ||
58            keyCode == keyEnd ||
59            keyCode == keyHome ||
60            keyCode == keyIns ||
61            keyCode == keyLeft ||
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 ||
71            keyCode == keyTab ||
72            keyCode == keyUp ||
73            (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
74            (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
75   }
77   /**
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.
82    */
83   function keystrokeToString(event) {
84     var output = [];
85     if (cr.isMac && event.metaKey)
86       output.push('Command');
87     if (cr.isChromeOS && event.metaKey)
88       output.push('Search');
89     if (event.ctrlKey)
90       output.push('Ctrl');
91     if (!event.ctrlKey && event.altKey)
92       output.push('Alt');
93     if (event.shiftKey)
94       output.push('Shift');
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));
101       } else {
102         switch (keyCode) {
103           case keyComma:
104             output.push('Comma'); break;
105           case keyDel:
106             output.push('Delete'); break;
107           case keyDown:
108             output.push('Down'); break;
109           case keyEnd:
110             output.push('End'); break;
111           case keyHome:
112             output.push('Home'); break;
113           case keyIns:
114             output.push('Insert'); break;
115           case keyLeft:
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;
123           case keyMediaStop:
124             output.push('MediaStop'); break;
125           case keyPageDown:
126             output.push('PageDown'); break;
127           case keyPageUp:
128             output.push('PageUp'); break;
129           case keyPeriod:
130             output.push('Period'); break;
131           case keyRight:
132             output.push('Right'); break;
133           case keySpace:
134             output.push('Space'); break;
135           case keyTab:
136             output.push('Tab'); break;
137           case keyUp:
138             output.push('Up'); break;
139         }
140       }
141     }
143     return output.join('+');
144   }
146   /**
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.
152    */
153   function modifiers(keyCode) {
154     switch (keyCode) {
155       case keyMediaNextTrack:
156       case keyMediaPlayPause:
157       case keyMediaPrevTrack:
158       case keyMediaStop:
159         return Modifiers.ARE_NOT_ALLOWED;
160       default:
161         return Modifiers.ARE_REQUIRED;
162     }
163   }
165   /**
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.
172    */
173   function hasModifier(event, countShiftAsModifier) {
174     return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
175            (cr.isChromeOS && event.metaKey) ||
176            (countShiftAsModifier && event.shiftKey);
177   }
179   /**
180    * Creates a new list of extension commands.
181    * @param {HTMLDivElement} div
182    * @constructor
183    * @extends {HTMLDivElement}
184    */
185   function ExtensionCommandList(div) {
186     div.__proto__ = ExtensionCommandList.prototype;
187     return div;
188   }
190   ExtensionCommandList.prototype = {
191     __proto__: HTMLDivElement.prototype,
193     /**
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}.
198      * @private
199      */
200     currentKeyEvent_: null,
202     /**
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.
205      * @type {string}.
206      * @private
207      */
208     oldValue_: '',
210     /**
211      * While capturing, this keeps track of which element the user asked to
212      * change.
213      * @type {HTMLElement}.
214      * @private
215      */
216     capturingElement_: null,
218     /**
219      * Updates the extensions data for the overlay.
220      * @param {!Array<ExtensionInfo>} data The extension data.
221      */
222     setData: function(data) {
223       /** @private {!Array<ExtensionInfo>} */
224       this.data_ = data;
226       this.textContent = '';
228       // Iterate over the extension data and add each item to the list.
229       this.data_.forEach(this.createNodeForExtension_.bind(this));
230     },
232     /**
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.
236      * @private
237      */
238     createNodeForExtension_: function(extension) {
239       if (extension.commands.length == 0 ||
240           extension.state == chrome.developerPrivate.ExtensionState.DISABLED)
241         return;
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
253       // to the list.
254       extension.commands.forEach(
255           this.createNodeForCommand_.bind(this, extension.id));
256     },
258     /**
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.
263      * @private
264      */
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');
287       } else {
288         shortcutNode.textContent = command.keybinding;
289       }
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;
310       } else {
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 ?
317                 1 : 0;
319         select.addEventListener(
320             'change', this.handleSetCommandScope_.bind(this));
321       }
323       this.appendChild(node);
324     },
326     /**
327      * Starts keystroke capture to determine which key to use for a particular
328      * extension command.
329      * @param {Event} event The keyboard event to consider.
330      * @private
331      */
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');
344       var commandClear =
345           shortcutNode.parentElement.querySelector('.command-clear');
346       commandClear.hidden = true;
348       this.capturingElement_ = /** @type {HTMLElement} */(event.target);
349     },
351     /**
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.
355      * @private
356      */
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
374       // canceled.
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;
382       } else {
383         commandShortcut.classList.add('clearable');
384         commandClear.hidden = false;
385       }
387       this.oldValue_ = '';
388       this.capturingElement_ = null;
389       this.currentKeyEvent_ = null;
390     },
392     /**
393      * Handles focus event and adds visual indication for active shortcut.
394      * @param {Event} event to consider.
395      * @private
396      */
397     handleFocus_: function(event) {
398       var commandShortcut = event.target.parentElement;
399       commandShortcut.classList.add('focused');
400     },
402     /**
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.
406      * @private
407      */
408     handleBlur_: function(event) {
409       this.endCapture_(event);
410       var commandShortcut = event.target.parentElement;
411       commandShortcut.classList.remove('focused');
412     },
414     /**
415      * The KeyDown handler.
416      * @param {Event} event The keyboard event to consider.
417      * @private
418      */
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).
425           return;
426         }
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,
434           keybinding: ''
435         });
436         event.preventDefault();
437         event.stopPropagation();
438         return;
439       }
440       if (event.keyCode == keyTab) {
441         // Allow tab propagation for keyboard navigation.
442         return;
443       }
445       if (!this.capturingElement_)
446         this.startCapture_(event);
448       this.handleKey_(event);
449     },
451     /**
452      * The KeyUp handler.
453      * @param {Event} event The keyboard event to consider.
454      * @private
455      */
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.
461         return;
462       }
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
476           // capture.
477           this.endCapture_(event);
478           this.startCapture_(event);
479         } else {
480           this.handleKey_(event);
481         }
482       }
483     },
485     /**
486      * A general key handler (used for both KeyDown and KeyUp).
487      * @param {KeyboardEvent} event The keyboard event to consider.
488      * @private
489      */
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.
500         return;
501       }
503       if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
504           hasModifier(event, true)) {
505         return;
506       }
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});
529       }
530     },
532     /**
533      * A handler for the delete command button.
534      * @param {Event} event The mouse event to consider.
535      * @private
536      */
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,
542            keybinding: ''});
543     },
545     /**
546      * A handler for the setting the scope of the command.
547      * @param {Event} event The mouse event to consider.
548      * @private
549      */
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,
560            scope: scope});
561     },
563     /**
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.
569      * @private
570      */
571     createElementId_: function(namespace, extensionId, commandName) {
572       return namespace + '-' + extensionId + '-' + commandName;
573     },
575     /**
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.
581      * @private
582      */
583     parseElementId_: function(namespace, id) {
584       var kExtensionIdLength = 32;
585       return {
586         extensionId: id.substring(namespace.length + 1,
587                                   namespace.length + 1 + kExtensionIdLength),
588         commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
589       };
590     },
591   };
593   return {
594     ExtensionCommandList: ExtensionCommandList
595   };