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'); |