diff options
Diffstat (limited to 'ext/js/input')
-rw-r--r-- | ext/js/input/hotkey-handler.js | 102 | ||||
-rw-r--r-- | ext/js/input/hotkey-help-controller.js | 92 | ||||
-rw-r--r-- | ext/js/input/hotkey-util.js | 64 |
3 files changed, 195 insertions, 63 deletions
diff --git a/ext/js/input/hotkey-handler.js b/ext/js/input/hotkey-handler.js index 2fd35a5c..f1512b8f 100644 --- a/ext/js/input/hotkey-handler.js +++ b/ext/js/input/hotkey-handler.js @@ -22,28 +22,25 @@ import {yomitan} from '../yomitan.js'; /** * Class which handles hotkey events and actions. + * @augments EventDispatcher<import('hotkey-handler').EventType> */ export class HotkeyHandler extends EventDispatcher { /** - * Information describing a hotkey. - * @typedef {object} HotkeyDefinition - * @property {string} action A string indicating which action to perform. - * @property {string} key A keyboard key code indicating which key needs to be pressed. - * @property {string[]} modifiers An array of keyboard modifiers which also need to be pressed. Supports: `'alt', 'ctrl', 'shift', 'meta'`. - * @property {string[]} scopes An array of scopes for which the hotkey is valid. If this array does not contain `this.scope`, the hotkey will not be registered. - * @property {boolean} enabled A boolean indicating whether the hotkey is currently enabled. - */ - - /** * Creates a new instance of the class. */ constructor() { super(); + /** @type {Map<string, (argument: unknown) => (boolean|void)>} */ this._actions = new Map(); + /** @type {Map<string, import('hotkey-handler').HotkeyHandlers>} */ this._hotkeys = new Map(); + /** @type {Map<import('settings').InputsHotkeyScope, import('settings').InputsHotkeyOptions[]>} */ this._hotkeyRegistrations = new Map(); + /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); + /** @type {boolean} */ this._isPrepared = false; + /** @type {boolean} */ this._hasEventListeners = false; } @@ -60,7 +57,7 @@ export class HotkeyHandler extends EventDispatcher { /** * Registers a set of actions that this hotkey handler supports. - * @param {*[][]} actions An array of `[name, handler]` entries, where `name` is a string and `handler` is a function. + * @param {[name: string, handler: (argument: unknown) => (boolean|void)][]} actions An array of `[name, handler]` entries, where `name` is a string and `handler` is a function. */ registerActions(actions) { for (const [name, handler] of actions) { @@ -70,8 +67,8 @@ export class HotkeyHandler extends EventDispatcher { /** * Registers a set of hotkeys for a given scope. - * @param {string} scope The scope that the hotkey definitions must be for in order to be activated. - * @param {HotkeyDefinition[]} hotkeys An array of hotkey definitions. + * @param {import('settings').InputsHotkeyScope} scope The scope that the hotkey definitions must be for in order to be activated. + * @param {import('settings').InputsHotkeyOptions[]} hotkeys An array of hotkey definitions. */ registerHotkeys(scope, hotkeys) { let registrations = this._hotkeyRegistrations.get(scope); @@ -85,7 +82,7 @@ export class HotkeyHandler extends EventDispatcher { /** * Removes all registered hotkeys for a given scope. - * @param {string} scope The scope that the hotkey definitions were registered in. + * @param {import('settings').InputsHotkeyScope} scope The scope that the hotkey definitions were registered in. */ clearHotkeys(scope) { const registrations = this._hotkeyRegistrations.get(scope); @@ -98,8 +95,8 @@ export class HotkeyHandler extends EventDispatcher { /** * Assigns a set of hotkeys for a given scope. This is an optimized shorthand for calling * `clearHotkeys`, then calling `registerHotkeys`. - * @param {string} scope The scope that the hotkey definitions must be for in order to be activated. - * @param {HotkeyDefinition[]} hotkeys An array of hotkey definitions. + * @param {import('settings').InputsHotkeyScope} scope The scope that the hotkey definitions must be for in order to be activated. + * @param {import('settings').InputsHotkeyOptions[]} hotkeys An array of hotkey definitions. */ setHotkeys(scope, hotkeys) { let registrations = this._hotkeyRegistrations.get(scope); @@ -109,14 +106,24 @@ export class HotkeyHandler extends EventDispatcher { } else { registrations.length = 0; } - registrations.push(...hotkeys); + for (const {action, argument, key, modifiers, scopes, enabled} of hotkeys) { + registrations.push({ + action, + argument, + key, + modifiers: [...modifiers], + scopes: [...scopes], + enabled + }); + } this._updateHotkeyRegistrations(); } /** * Adds a single event listener to a specific event. - * @param {string} eventName The string representing the event's name. - * @param {Function} callback The event listener callback to add. + * @template [TEventDetails=unknown] + * @param {import('hotkey-handler').EventType} eventName The string representing the event's name. + * @param {(details: TEventDetails) => void} callback The event listener callback to add. * @returns {void} */ on(eventName, callback) { @@ -128,8 +135,9 @@ export class HotkeyHandler extends EventDispatcher { /** * Removes a single event listener from a specific event. - * @param {string} eventName The string representing the event's name. - * @param {Function} callback The event listener callback to add. + * @template [TEventDetails=unknown] + * @param {import('hotkey-handler').EventType} eventName The string representing the event's name. + * @param {(details: TEventDetails) => void} callback The event listener callback to add. * @returns {boolean} `true` if the callback was removed, `false` otherwise. */ off(eventName, callback) { @@ -142,37 +150,50 @@ export class HotkeyHandler extends EventDispatcher { /** * Attempts to simulate an action for a given combination of key and modifiers. * @param {string} key A keyboard key code indicating which key needs to be pressed. - * @param {string[]} modifiers An array of keyboard modifiers which also need to be pressed. Supports: `'alt', 'ctrl', 'shift', 'meta'`. + * @param {import('input').ModifierKey[]} modifiers An array of keyboard modifiers which also need to be pressed. Supports: `'alt', 'ctrl', 'shift', 'meta'`. * @returns {boolean} `true` if an action was performed, `false` otherwise. */ simulate(key, modifiers) { const hotkeyInfo = this._hotkeys.get(key); return ( typeof hotkeyInfo !== 'undefined' && - this._invokeHandlers(modifiers, hotkeyInfo) + this._invokeHandlers(modifiers, hotkeyInfo, key) ); } // Message handlers + /** + * @param {{key: string, modifiers: import('input').ModifierKey[]}} details + * @returns {boolean} + */ _onMessageForwardHotkey({key, modifiers}) { return this.simulate(key, modifiers); } // Private - _onKeyDown(e) { - const hotkeyInfo = this._hotkeys.get(e.code); + /** + * @param {KeyboardEvent} event + */ + _onKeyDown(event) { + const hotkeyInfo = this._hotkeys.get(event.code); if (typeof hotkeyInfo !== 'undefined') { - const eventModifiers = DocumentUtil.getActiveModifiers(e); - if (this._invokeHandlers(eventModifiers, hotkeyInfo, e.key)) { - e.preventDefault(); + const eventModifiers = DocumentUtil.getActiveModifiers(event); + if (this._invokeHandlers(eventModifiers, hotkeyInfo, event.key)) { + event.preventDefault(); return; } } - this.trigger('keydownNonHotkey', e); + this.trigger('keydownNonHotkey', event); } + /** + * @param {import('input').ModifierKey[]} modifiers + * @param {import('hotkey-handler').HotkeyHandlers} hotkeyInfo + * @param {string} key + * @returns {boolean} + */ _invokeHandlers(modifiers, hotkeyInfo, key) { for (const {modifiers: handlerModifiers, action, argument} of hotkeyInfo.handlers) { if (!this._areSame(handlerModifiers, modifiers) || !this._isHotkeyPermitted(modifiers, key)) { continue; } @@ -189,6 +210,11 @@ export class HotkeyHandler extends EventDispatcher { return false; } + /** + * @param {Set<unknown>} set + * @param {unknown[]} array + * @returns {boolean} + */ _areSame(set, array) { if (set.size !== array.length) { return false; } for (const value of array) { @@ -199,6 +225,9 @@ export class HotkeyHandler extends EventDispatcher { return true; } + /** + * @returns {void} + */ _updateHotkeyRegistrations() { if (this._hotkeys.size === 0 && this._hotkeyRegistrations.size === 0) { return; } @@ -219,10 +248,16 @@ export class HotkeyHandler extends EventDispatcher { this._updateEventHandlers(); } + /** + * @returns {void} + */ _updateHasEventListeners() { this._hasEventListeners = this.hasListeners('keydownNonHotkey'); } + /** + * @returns {void} + */ _updateEventHandlers() { if (this._isPrepared && (this._hotkeys.size > 0 || this._hasEventListeners)) { if (this._eventListeners.size !== 0) { return; } @@ -232,6 +267,11 @@ export class HotkeyHandler extends EventDispatcher { } } + /** + * @param {import('input').ModifierKey[]} modifiers + * @param {string} key + * @returns {boolean} + */ _isHotkeyPermitted(modifiers, key) { return !( (modifiers.length === 0 || (modifiers.length === 1 && modifiers[0] === 'shift')) && @@ -240,6 +280,10 @@ export class HotkeyHandler extends EventDispatcher { ); } + /** + * @param {string} key + * @returns {boolean} + */ _isKeyCharacterInput(key) { return key.length === 1; } diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index 51ec8fac..4a3c0264 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -22,21 +22,31 @@ import {HotkeyUtil} from './hotkey-util.js'; export class HotkeyHelpController { constructor() { + /** @type {HotkeyUtil} */ this._hotkeyUtil = new HotkeyUtil(); - this._localActionHotseys = new Map(); + /** @type {Map<string, string>} */ + this._localActionHotkeys = new Map(); + /** @type {Map<string, string>} */ this._globalActionHotkeys = new Map(); + /** @type {RegExp} */ this._replacementPattern = /\{0\}/g; } + /** + * @returns {Promise<void>} + */ async prepare() { const {platform: {os}} = await yomitan.api.getEnvironmentInfo(); this._hotkeyUtil.os = os; await this._setupGlobalCommands(this._globalActionHotkeys); } + /** + * @param {import('settings').ProfileOptions} options + */ setOptions(options) { const hotkeys = options.inputs.hotkeys; - const hotkeyMap = this._localActionHotseys; + const hotkeyMap = this._localActionHotkeys; hotkeyMap.clear(); for (const {enabled, action, key, modifiers} of hotkeys) { if (!enabled || key === null || action === '' || hotkeyMap.has(action)) { continue; } @@ -44,28 +54,25 @@ export class HotkeyHelpController { } } + /** + * @param {ParentNode} node + */ setupNode(node) { - const globalPrexix = 'global:'; const replacementPattern = this._replacementPattern; - for (const node2 of node.querySelectorAll('[data-hotkey]')) { - const data = JSON.parse(node2.dataset.hotkey); - let [action, attributes, values] = data; - if (!Array.isArray(attributes)) { attributes = [attributes]; } + for (const node2 of /** @type {NodeListOf<HTMLElement>} */ (node.querySelectorAll('[data-hotkey]'))) { + const info = this._getNodeInfo(node2); + if (info === null) { continue; } + const {action, global, attributes, values, defaultAttributeValues} = info; const multipleValues = Array.isArray(values); - - const actionIsGlobal = action.startsWith(globalPrexix); - if (actionIsGlobal) { action = action.substring(globalPrexix.length); } - - const defaultAttributeValues = this._getDefaultAttributeValues(node2, data, attributes); - - const hotkey = (actionIsGlobal ? this._globalActionHotkeys : this._localActionHotseys).get(action); - + const hotkey = (global ? this._globalActionHotkeys : this._localActionHotkeys).get(action); for (let i = 0, ii = attributes.length; i < ii; ++i) { const attribute = attributes[i]; - let value = null; + let value; if (typeof hotkey !== 'undefined') { - value = (multipleValues ? values[i] : values); - value = value.replace(replacementPattern, hotkey); + value = /** @type {unknown} */ (multipleValues ? values[i] : values); + if (typeof value === 'string') { + value = value.replace(replacementPattern, hotkey); + } } else { value = defaultAttributeValues[i]; } @@ -81,6 +88,9 @@ export class HotkeyHelpController { // Private + /** + * @param {Map<string, string>} commandMap + */ async _setupGlobalCommands(commandMap) { const commands = await new Promise((resolve, reject) => { if (!(isObject(chrome.commands) && typeof chrome.commands.getAll === 'function')) { @@ -104,14 +114,23 @@ export class HotkeyHelpController { const {key, modifiers} = this._hotkeyUtil.convertCommandToInput(shortcut); commandMap.set(name, this._hotkeyUtil.getInputDisplayValue(key, modifiers)); } - return commandMap; } + /** + * @param {HTMLElement} node + * @param {unknown[]} data + * @param {string[]} attributes + * @returns {unknown[]} + */ _getDefaultAttributeValues(node, data, attributes) { if (data.length > 3) { - return data[3]; + const result = data[3]; + if (Array.isArray(result)) { + return result; + } } + /** @type {(?string)[]} */ const defaultAttributeValues = []; for (let i = 0, ii = attributes.length; i < ii; ++i) { const attribute = attributes[i]; @@ -122,4 +141,37 @@ export class HotkeyHelpController { node.dataset.hotkey = JSON.stringify(data); return defaultAttributeValues; } + + /** + * @param {HTMLElement} node + * @returns {?{action: string, global: boolean, attributes: string[], values: unknown, defaultAttributeValues: unknown[]}} + */ + _getNodeInfo(node) { + const {hotkey} = node.dataset; + if (typeof hotkey !== 'string') { return null; } + const data = /** @type {unknown} */ (JSON.parse(hotkey)); + if (!Array.isArray(data)) { return null; } + const [action, attributes, values] = data; + if (typeof action !== 'string') { return null; } + /** @type {string[]} */ + const attributesArray = []; + if (Array.isArray(attributes)) { + for (const item of attributes) { + if (typeof item !== 'string') { continue; } + attributesArray.push(item); + } + } else if (typeof attributes === 'string') { + attributesArray.push(attributes); + } + const defaultAttributeValues = this._getDefaultAttributeValues(node, data, attributesArray); + const globalPrexix = 'global:'; + const global = action.startsWith(globalPrexix); + return { + action: global ? action.substring(globalPrexix.length) : action, + global, + attributes, + values, + defaultAttributeValues + }; + } } diff --git a/ext/js/input/hotkey-util.js b/ext/js/input/hotkey-util.js index e23849e0..10328924 100644 --- a/ext/js/input/hotkey-util.js +++ b/ext/js/input/hotkey-util.js @@ -22,19 +22,25 @@ export class HotkeyUtil { /** * Creates a new instance. - * @param {?string} os The operating system for this instance. + * @param {?import('environment').OperatingSystem} os The operating system for this instance. */ constructor(os=null) { + /** @type {?import('environment').OperatingSystem} */ this._os = os; + /** @type {string} */ this._inputSeparator = ' + '; + /** @type {Map<import('input').Modifier, string>} */ this._modifierKeyNames = new Map(); + /** @type {RegExp} */ this._mouseInputNamePattern = /^mouse(\d+)$/; + /** @type {Map<import('input').Modifier, number>} */ this._modifierPriorities = new Map([ ['meta', -4], ['ctrl', -3], ['alt', -2], ['shift', -1] ]); + /** @type {Intl.Collator} */ this._stringComparer = new Intl.Collator('en-US'); // Invariant locale this._updateModifierKeyNames(); @@ -43,7 +49,7 @@ export class HotkeyUtil { /** * Gets the operating system for this instance. * The operating system is used to display system-localized modifier key names. - * @type {?string} + * @type {?import('environment').OperatingSystem} */ get os() { return this._os; @@ -51,7 +57,7 @@ export class HotkeyUtil { /** * Sets the operating system for this instance. - * @param {?string} value The value to assign. + * @param {?import('environment').OperatingSystem} value The value to assign. * Valid values are: win, mac, linux, openbsd, cros, android. */ set os(value) { @@ -63,7 +69,7 @@ export class HotkeyUtil { /** * Gets a display string for a key and a set of modifiers. * @param {?string} key The key code string, or `null` for no key. - * @param {string[]} modifiers An array of modifiers. + * @param {import('input').Modifier[]} modifiers An array of modifiers. * Valid values are: ctrl, alt, shift, meta, or mouseN, where N is an integer. * @returns {string} A user-friendly string for the combination of key and modifiers. */ @@ -88,7 +94,7 @@ export class HotkeyUtil { /** * Gets a display string for a single modifier. - * @param {string} modifier A string representing a modifier. + * @param {import('input').Modifier} modifier A string representing a modifier. * Valid values are: ctrl, alt, shift, meta, or mouseN, where N is an integer. * @returns {string} A user-friendly string for the modifier. */ @@ -116,9 +122,9 @@ export class HotkeyUtil { /** * Gets a display string for a single modifier. - * @param {string} modifier A string representing a modifier. + * @param {import('input').Modifier} modifier A string representing a modifier. * Valid values are: ctrl, alt, shift, meta, or mouseN, where N is an integer. - * @returns {'mouse'|'key'} `'mouse'` if the modifier represents a mouse button, `'key'` otherwise. + * @returns {import('input').ModifierType} `'mouse'` if the modifier represents a mouse button, `'key'` otherwise. */ getModifierType(modifier) { return (this._mouseInputNamePattern.test(modifier) ? 'mouse' : 'key'); @@ -126,18 +132,22 @@ export class HotkeyUtil { /** * Converts an extension command string into a standard input. - * @param {string} command An extension command string. - * @returns {{key: ?string, modifiers: string[]}} An object `{key, modifiers}`, where key is a string (or `null`) representing the key, and modifiers is an array of modifier keys. + * @param {string|undefined} command An extension command string. + * @returns {{key: ?string, modifiers: import('input').ModifierKey[]}} An object `{key, modifiers}`, where key is a string (or `null`) representing the key, and modifiers is an array of modifier keys. */ convertCommandToInput(command) { let key = null; + /** @type {Set<import('input').ModifierKey>} */ const modifiers = new Set(); if (typeof command === 'string' && command.length > 0) { const parts = command.split('+'); const ii = parts.length - 1; key = this._convertCommandKeyToInputKey(parts[ii]); for (let i = 0; i < ii; ++i) { - modifiers.add(this._convertCommandModifierToInputModifier(parts[i])); + const modifier = this._convertCommandModifierToInputModifier(parts[i]); + if (modifier !== null) { + modifiers.add(modifier); + } } } return {key, modifiers: this.sortModifiers([...modifiers])}; @@ -146,7 +156,7 @@ export class HotkeyUtil { /** * Gets a command string for a specified input. * @param {?string} key The key code string, or `null` for no key. - * @param {string[]} modifiers An array of modifier keys. + * @param {import('input').Modifier[]} modifiers An array of modifier keys. * Valid values are: ctrl, alt, shift, meta. * @returns {string} An extension command string representing the input. */ @@ -170,10 +180,11 @@ export class HotkeyUtil { } /** + * @template {import('input').Modifier} TModifier * Sorts an array of modifiers. - * @param {string[]} modifiers An array of modifiers. + * @param {TModifier[]} modifiers An array of modifiers. * Valid values are: ctrl, alt, shift, meta. - * @returns {string[]} A sorted array of modifiers. The array instance is the same as the input array. + * @returns {TModifier[]} A sorted array of modifiers. The array instance is the same as the input array. */ sortModifiers(modifiers) { const pattern = this._mouseInputNamePattern; @@ -181,10 +192,12 @@ export class HotkeyUtil { const stringComparer = this._stringComparer; const count = modifiers.length; + /** @type {[modifier: TModifier, isMouse: 0|1, priority: number, index: number][]} */ const modifierInfos = []; for (let i = 0; i < count; ++i) { const modifier = modifiers[i]; const match = pattern.exec(modifier); + /** @type {[modifier: TModifier, isMouse: 0|1, priority: number, index: number]} */ let info; if (match !== null) { info = [modifier, 1, Number.parseInt(match[1], 10), i]; @@ -219,6 +232,10 @@ export class HotkeyUtil { // Private + /** + * @param {?import('environment').OperatingSystem} os + * @returns {[modifier: import('input').ModifierKey, label: string][]} + */ _getModifierKeyNames(os) { switch (os) { case 'win': @@ -255,6 +272,9 @@ export class HotkeyUtil { } } + /** + * @returns {void} + */ _updateModifierKeyNames() { const map = this._modifierKeyNames; map.clear(); @@ -263,6 +283,10 @@ export class HotkeyUtil { } } + /** + * @param {string} key + * @returns {string} + */ _convertCommandKeyToInputKey(key) { if (key.length === 1) { key = `Key${key}`; @@ -270,6 +294,10 @@ export class HotkeyUtil { return key; } + /** + * @param {string} modifier + * @returns {?import('input').ModifierKey} + */ _convertCommandModifierToInputModifier(modifier) { switch (modifier) { case 'Ctrl': return (this._os === 'mac' ? 'meta' : 'ctrl'); @@ -277,10 +305,14 @@ export class HotkeyUtil { case 'Shift': return 'shift'; case 'MacCtrl': return 'ctrl'; case 'Command': return 'meta'; - default: return modifier; + default: return null; } } + /** + * @param {string} key + * @returns {string} + */ _convertInputKeyToCommandKey(key) { if (key.length === 4 && key.startsWith('Key')) { key = key.substring(3); @@ -288,6 +320,10 @@ export class HotkeyUtil { return key; } + /** + * @param {import('input').Modifier} modifier + * @returns {string} + */ _convertInputModifierToCommandModifier(modifier) { switch (modifier) { case 'ctrl': return (this._os === 'mac' ? 'MacCtrl' : 'Ctrl'); |