blob: fd91fc0c9f843b0bc6c103edcdbcb0e4d3e11403 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
<include src="shortcut_util.js">
cr.define('extensions', function() {
'use strict';
/**
* Creates a new list of extension commands.
* @param {HTMLDivElement} div
* @constructor
* @extends {HTMLDivElement}
*/
function ExtensionCommandList(div) {
div.__proto__ = ExtensionCommandList.prototype;
return div;
}
ExtensionCommandList.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* While capturing, this records the current (last) keyboard event generated
* by the user. Will be |null| after capture and during capture when no
* keyboard event has been generated.
* @type {KeyboardEvent}.
* @private
*/
currentKeyEvent_: null,
/**
* While capturing, this keeps track of the previous selection so we can
* revert back to if no valid assignment is made during capture.
* @type {string}.
* @private
*/
oldValue_: '',
/**
* While capturing, this keeps track of which element the user asked to
* change.
* @type {HTMLElement}.
* @private
*/
capturingElement_: null,
/**
* Updates the extensions data for the overlay.
* @param {!Array<chrome.developerPrivate.ExtensionInfo>} data The extension
* data.
*/
setData: function(data) {
/** @private {!Array<chrome.developerPrivate.ExtensionInfo>} */
this.data_ = data;
this.textContent = '';
// Iterate over the extension data and add each item to the list.
this.data_.forEach(this.createNodeForExtension_.bind(this));
},
/**
* Synthesizes and initializes an HTML element for the extension command
* metadata given in |extension|.
* @param {chrome.developerPrivate.ExtensionInfo} extension A dictionary of
* extension metadata.
* @private
*/
createNodeForExtension_: function(extension) {
if (extension.commands.length == 0 ||
extension.state == chrome.developerPrivate.ExtensionState.DISABLED)
return;
var template = $('template-collection-extension-commands').querySelector(
'.extension-command-list-extension-item-wrapper');
var node = template.cloneNode(true);
var title = node.querySelector('.extension-title');
title.textContent = extension.name;
this.appendChild(node);
// Iterate over the commands data within the extension and add each item
// to the list.
extension.commands.forEach(
this.createNodeForCommand_.bind(this, extension.id));
},
/**
* Synthesizes and initializes an HTML element for the extension command
* metadata given in |command|.
* @param {string} extensionId The associated extension's id.
* @param {chrome.developerPrivate.Command} command A dictionary of
* extension command metadata.
* @private
*/
createNodeForCommand_: function(extensionId, command) {
var template = $('template-collection-extension-commands').querySelector(
'.extension-command-list-command-item-wrapper');
var node = template.cloneNode(true);
node.id = this.createElementId_('command', extensionId, command.name);
var description = node.querySelector('.command-description');
description.textContent = command.description;
var shortcutNode = node.querySelector('.command-shortcut-text');
shortcutNode.addEventListener('mouseup',
this.startCapture_.bind(this));
shortcutNode.addEventListener('focus', this.handleFocus_.bind(this));
shortcutNode.addEventListener('blur', this.handleBlur_.bind(this));
shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this));
shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this));
if (!command.isActive) {
shortcutNode.textContent =
loadTimeData.getString('extensionCommandsInactive');
var commandShortcut = node.querySelector('.command-shortcut');
commandShortcut.classList.add('inactive-keybinding');
} else {
shortcutNode.textContent = command.keybinding;
}
var commandClear = node.querySelector('.command-clear');
commandClear.id = this.createElementId_(
'clear', extensionId, command.name);
commandClear.title = loadTimeData.getString('extensionCommandsDelete');
commandClear.addEventListener('click', this.handleClear_.bind(this));
var select = node.querySelector('.command-scope');
select.id = this.createElementId_(
'setCommandScope', extensionId, command.name);
select.hidden = false;
// Add the 'In Chrome' option.
var option = document.createElement('option');
option.textContent = loadTimeData.getString('extensionCommandsRegular');
select.appendChild(option);
if (command.isExtensionAction || !command.isActive) {
// Extension actions cannot be global, so we might as well disable the
// combo box, to signify that, and if the command is inactive, it
// doesn't make sense to allow the user to adjust the scope.
select.disabled = true;
} else {
// Add the 'Global' option.
option = document.createElement('option');
option.textContent = loadTimeData.getString('extensionCommandsGlobal');
select.appendChild(option);
select.selectedIndex =
command.scope == chrome.developerPrivate.CommandScope.GLOBAL ?
1 : 0;
select.addEventListener(
'change', this.handleSetCommandScope_.bind(this));
}
this.appendChild(node);
},
/**
* Starts keystroke capture to determine which key to use for a particular
* extension command.
* @param {Event} event The keyboard event to consider.
* @private
*/
startCapture_: function(event) {
if (this.capturingElement_)
return; // Already capturing.
chrome.developerPrivate.setShortcutHandlingSuspended(true);
var shortcutNode = event.target;
this.oldValue_ = shortcutNode.textContent;
shortcutNode.textContent =
loadTimeData.getString('extensionCommandsStartTyping');
shortcutNode.parentElement.classList.add('capturing');
var commandClear =
shortcutNode.parentElement.querySelector('.command-clear');
commandClear.hidden = true;
this.capturingElement_ = /** @type {HTMLElement} */(event.target);
},
/**
* Ends keystroke capture and either restores the old value or (if valid
* value) sets the new value as active..
* @param {Event} event The keyboard event to consider.
* @private
*/
endCapture_: function(event) {
if (!this.capturingElement_)
return; // Not capturing.
chrome.developerPrivate.setShortcutHandlingSuspended(false);
var shortcutNode = this.capturingElement_;
var commandShortcut = shortcutNode.parentElement;
commandShortcut.classList.remove('capturing');
commandShortcut.classList.remove('contains-chars');
// When the capture ends, the user may have not given a complete and valid
// input (or even no input at all). Only a valid key event followed by a
// valid key combination will cause a shortcut selection to be activated.
// If no valid selection was made, however, revert back to what the
// textbox had before to indicate that the shortcut registration was
// canceled.
if (!this.currentKeyEvent_ ||
!extensions.isValidKeyCode(this.currentKeyEvent_.keyCode))
shortcutNode.textContent = this.oldValue_;
var commandClear = commandShortcut.querySelector('.command-clear');
if (this.oldValue_ == '') {
commandShortcut.classList.remove('clearable');
commandClear.hidden = true;
} else {
commandShortcut.classList.add('clearable');
commandClear.hidden = false;
}
this.oldValue_ = '';
this.capturingElement_ = null;
this.currentKeyEvent_ = null;
},
/**
* Handles focus event and adds visual indication for active shortcut.
* @param {Event} event to consider.
* @private
*/
handleFocus_: function(event) {
var commandShortcut = event.target.parentElement;
commandShortcut.classList.add('focused');
},
/**
* Handles lost focus event and removes visual indication of active shortcut
* also stops capturing on focus lost.
* @param {Event} event to consider.
* @private
*/
handleBlur_: function(event) {
this.endCapture_(event);
var commandShortcut = event.target.parentElement;
commandShortcut.classList.remove('focused');
},
/**
* The KeyDown handler.
* @param {Event} event The keyboard event to consider.
* @private
*/
handleKeyDown_: function(event) {
event = /** @type {KeyboardEvent} */(event);
if (event.keyCode == extensions.Key.Escape) {
if (!this.capturingElement_) {
// If we're not currently capturing, allow escape to propagate (so it
// can close the overflow).
return;
}
// Otherwise, escape cancels capturing.
this.endCapture_(event);
var parsed = this.parseElementId_('clear',
event.target.parentElement.querySelector('.command-clear').id);
chrome.developerPrivate.updateExtensionCommand({
extensionId: parsed.extensionId,
commandName: parsed.commandName,
keybinding: ''
});
event.preventDefault();
event.stopPropagation();
return;
}
if (event.keyCode == extensions.Key.Tab) {
// Allow tab propagation for keyboard navigation.
return;
}
if (!this.capturingElement_)
this.startCapture_(event);
this.handleKey_(event);
},
/**
* The KeyUp handler.
* @param {Event} event The keyboard event to consider.
* @private
*/
handleKeyUp_: function(event) {
event = /** @type {KeyboardEvent} */(event);
if (event.keyCode == extensions.Key.Tab ||
event.keyCode == extensions.Key.Escape) {
// We need to allow tab propagation for keyboard navigation, and escapes
// are fully handled in handleKeyDown.
return;
}
// We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by
// releasing Shift, but we also don't want it to be easy to lose for
// example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl
// as fast as the other two keys. Therefore, we process KeyUp until you
// have a valid combination and then stop processing it (meaning that once
// you have a valid combination, we won't change it until the next
// KeyDown message arrives).
if (!this.currentKeyEvent_ ||
!extensions.isValidKeyCode(this.currentKeyEvent_.keyCode)) {
if (!event.ctrlKey && !event.altKey ||
((cr.isMac || cr.isChromeOS) && !event.metaKey)) {
// If neither Ctrl nor Alt is pressed then it is not a valid shortcut.
// That means we're back at the starting point so we should restart
// capture.
this.endCapture_(event);
this.startCapture_(event);
} else {
this.handleKey_(event);
}
}
},
/**
* A general key handler (used for both KeyDown and KeyUp).
* @param {KeyboardEvent} event The keyboard event to consider.
* @private
*/
handleKey_: function(event) {
// While capturing, we prevent all events from bubbling, to prevent
// shortcuts lacking the right modifier (F3 for example) from activating
// and ending capture prematurely.
event.preventDefault();
event.stopPropagation();
if (!extensions.hasValidModifiers(event))
return;
var shortcutNode = this.capturingElement_;
var keystroke = extensions.keystrokeToString(event);
shortcutNode.textContent = keystroke;
event.target.classList.add('contains-chars');
this.currentKeyEvent_ = event;
if (extensions.isValidKeyCode(event.keyCode)) {
var node = event.target;
while (node && !node.id)
node = node.parentElement;
this.oldValue_ = keystroke; // Forget what the old value was.
var parsed = this.parseElementId_('command', node.id);
// Ending the capture must occur before calling
// setExtensionCommandShortcut to ensure the shortcut is set.
this.endCapture_(event);
chrome.developerPrivate.updateExtensionCommand(
{extensionId: parsed.extensionId,
commandName: parsed.commandName,
keybinding: keystroke});
}
},
/**
* A handler for the delete command button.
* @param {Event} event The mouse event to consider.
* @private
*/
handleClear_: function(event) {
var parsed = this.parseElementId_('clear', event.target.id);
chrome.developerPrivate.updateExtensionCommand(
{extensionId: parsed.extensionId,
commandName: parsed.commandName,
keybinding: ''});
},
/**
* A handler for the setting the scope of the command.
* @param {Event} event The mouse event to consider.
* @private
*/
handleSetCommandScope_: function(event) {
var parsed = this.parseElementId_('setCommandScope', event.target.id);
var element = document.getElementById(
'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName);
var scope = element.selectedIndex == 1 ?
chrome.developerPrivate.CommandScope.GLOBAL :
chrome.developerPrivate.CommandScope.CHROME;
chrome.developerPrivate.updateExtensionCommand(
{extensionId: parsed.extensionId,
commandName: parsed.commandName,
scope: scope});
},
/**
* A utility function to create a unique element id based on a namespace,
* extension id and a command name.
* @param {string} namespace The namespace to prepend the id with.
* @param {string} extensionId The extension ID to use in the id.
* @param {string} commandName The command name to append the id with.
* @private
*/
createElementId_: function(namespace, extensionId, commandName) {
return namespace + '-' + extensionId + '-' + commandName;
},
/**
* A utility function to parse a unique element id based on a namespace,
* extension id and a command name.
* @param {string} namespace The namespace to prepend the id with.
* @param {string} id The id to parse.
* @return {{extensionId: string, commandName: string}} The parsed id.
* @private
*/
parseElementId_: function(namespace, id) {
var kExtensionIdLength = 32;
return {
extensionId: id.substring(namespace.length + 1,
namespace.length + 1 + kExtensionIdLength),
commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
};
},
};
return {
ExtensionCommandList: ExtensionCommandList
};
});