diff options
| author | Darius Jahandarie <djahandarie@gmail.com> | 2023-12-06 03:53:16 +0000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-06 03:53:16 +0000 | 
| commit | bd5bc1a5db29903bc098995cd9262c4576bf76af (patch) | |
| tree | c9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/pages/settings/keyboard-shortcuts-controller.js | |
| parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
| parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) | |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/pages/settings/keyboard-shortcuts-controller.js')
| -rw-r--r-- | ext/js/pages/settings/keyboard-shortcuts-controller.js | 268 | 
1 files changed, 226 insertions, 42 deletions
| diff --git a/ext/js/pages/settings/keyboard-shortcuts-controller.js b/ext/js/pages/settings/keyboard-shortcuts-controller.js index e7ad4d15..ad16b0e9 100644 --- a/ext/js/pages/settings/keyboard-shortcuts-controller.js +++ b/ext/js/pages/settings/keyboard-shortcuts-controller.js @@ -23,16 +23,29 @@ import {yomitan} from '../../yomitan.js';  import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';  export class KeyboardShortcutController { +    /** +     * @param {import('./settings-controller.js').SettingsController} settingsController +     */      constructor(settingsController) { +        /** @type {import('./settings-controller.js').SettingsController} */          this._settingsController = settingsController; +        /** @type {KeyboardShortcutHotkeyEntry[]} */          this._entries = []; +        /** @type {?import('environment').OperatingSystem} */          this._os = null; +        /** @type {?HTMLButtonElement} */          this._addButton = null; +        /** @type {?HTMLButtonElement} */          this._resetButton = null; +        /** @type {?HTMLElement} */          this._listContainer = null; +        /** @type {?HTMLElement} */          this._emptyIndicator = null; +        /** @type {Intl.Collator} */          this._stringComparer = new Intl.Collator('en-US'); // Invariant locale +        /** @type {?HTMLElement} */          this._scrollContainer = null; +        /** @type {Map<string, import('keyboard-shortcut-controller').ActionDetails>} */          this._actionDetails = new Map([              ['',                                 {scopes: new Set()}],              ['close',                            {scopes: new Set(['popup', 'search'])}], @@ -58,19 +71,21 @@ export class KeyboardShortcutController {          ]);      } +    /** @type {import('./settings-controller.js').SettingsController} */      get settingsController() {          return this._settingsController;      } +    /** */      async prepare() {          const {platform: {os}} = await yomitan.api.getEnvironmentInfo();          this._os = os; -        this._addButton = document.querySelector('#hotkey-list-add'); -        this._resetButton = document.querySelector('#hotkey-list-reset'); -        this._listContainer = document.querySelector('#hotkey-list'); -        this._emptyIndicator = document.querySelector('#hotkey-list-empty'); -        this._scrollContainer = document.querySelector('#keyboard-shortcuts-modal .modal-body'); +        this._addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#hotkey-list-add')); +        this._resetButton = /** @type {HTMLButtonElement} */ (document.querySelector('#hotkey-list-reset')); +        this._listContainer = /** @type {HTMLElement} */ (document.querySelector('#hotkey-list')); +        this._emptyIndicator = /** @type {HTMLElement} */ (document.querySelector('#hotkey-list-empty')); +        this._scrollContainer = /** @type {HTMLElement} */ (document.querySelector('#keyboard-shortcuts-modal .modal-body'));          this._addButton.addEventListener('click', this._onAddClick.bind(this));          this._resetButton.addEventListener('click', this._onResetClick.bind(this)); @@ -79,6 +94,9 @@ export class KeyboardShortcutController {          await this._updateOptions();      } +    /** +     * @param {import('settings').InputsHotkeyOptions} terminationCharacterEntry +     */      async addEntry(terminationCharacterEntry) {          const options = await this._settingsController.getOptions();          const {inputs: {hotkeys}} = options; @@ -92,9 +110,14 @@ export class KeyboardShortcutController {          }]);          await this._updateOptions(); -        this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight; +        const scrollContainer = /** @type {HTMLElement} */ (this._scrollContainer); +        scrollContainer.scrollTop = scrollContainer.scrollHeight;      } +    /** +     * @param {number} index +     * @returns {Promise<boolean>} +     */      async deleteEntry(index) {          const options = await this._settingsController.getOptions();          const {inputs: {hotkeys}} = options; @@ -113,55 +136,79 @@ export class KeyboardShortcutController {          return true;      } +    /** +     * @param {import('settings-modifications').Modification[]} targets +     * @returns {Promise<import('settings-controller').ModifyResult[]>} +     */      async modifyProfileSettings(targets) {          return await this._settingsController.modifyProfileSettings(targets);      } +    /** +     * @returns {Promise<import('settings').InputsHotkeyOptions[]>} +     */      async getDefaultHotkeys() {          const defaultOptions = await this._settingsController.getDefaultOptions();          return defaultOptions.profiles[0].options.inputs.hotkeys;      } +    /** +     * @param {string} action +     * @returns {import('keyboard-shortcut-controller').ActionDetails|undefined} +     */      getActionDetails(action) {          return this._actionDetails.get(action);      }      // Private +    /** +     * @param {import('settings-controller').OptionsChangedEvent} details +     */      _onOptionsChanged({options}) {          for (const entry of this._entries) {              entry.cleanup();          }          this._entries = []; +        const os = /** @type {import('environment').OperatingSystem} */ (this._os);          const {inputs: {hotkeys}} = options;          const fragment = document.createDocumentFragment();          for (let i = 0, ii = hotkeys.length; i < ii; ++i) {              const hotkeyEntry = hotkeys[i]; -            const node = this._settingsController.instantiateTemplate('hotkey-list-item'); +            const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('hotkey-list-item'));              fragment.appendChild(node); -            const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, this._os, this._stringComparer); +            const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, os, this._stringComparer);              this._entries.push(entry);              entry.prepare();          } -        this._listContainer.appendChild(fragment); -        this._listContainer.hidden = (hotkeys.length === 0); -        this._emptyIndicator.hidden = (hotkeys.length !== 0); +        const listContainer = /** @type {HTMLElement} */ (this._listContainer); +        listContainer.appendChild(fragment); +        listContainer.hidden = (hotkeys.length === 0); +        /** @type {HTMLElement} */ (this._emptyIndicator).hidden = (hotkeys.length !== 0);      } +    /** +     * @param {MouseEvent} e +     */      _onAddClick(e) {          e.preventDefault();          this._addNewEntry();      } +    /** +     * @param {MouseEvent} e +     */      _onResetClick(e) {          e.preventDefault();          this._reset();      } +    /** */      async _addNewEntry() { +        /** @type {import('settings').InputsHotkeyOptions} */          const newEntry = {              action: '',              argument: '', @@ -170,14 +217,17 @@ export class KeyboardShortcutController {              scopes: ['popup', 'search'],              enabled: true          }; -        return await this.addEntry(newEntry); +        await this.addEntry(newEntry);      } +    /** */      async _updateOptions() {          const options = await this._settingsController.getOptions(); -        this._onOptionsChanged({options}); +        const optionsContext = this._settingsController.getOptionsContext(); +        this._onOptionsChanged({options, optionsContext});      } +    /** */      async _reset() {          const value = await this.getDefaultHotkeys();          await this._settingsController.setProfileSetting('inputs.hotkeys', value); @@ -186,34 +236,59 @@ export class KeyboardShortcutController {  }  class KeyboardShortcutHotkeyEntry { +    /** +     * @param {KeyboardShortcutController} parent +     * @param {import('settings').InputsHotkeyOptions} data +     * @param {number} index +     * @param {HTMLElement} node +     * @param {import('environment').OperatingSystem} os +     * @param {Intl.Collator} stringComparer +     */      constructor(parent, data, index, node, os, stringComparer) { +        /** @type {KeyboardShortcutController} */          this._parent = parent; +        /** @type {import('settings').InputsHotkeyOptions} */          this._data = data; +        /** @type {number} */          this._index = index; +        /** @type {HTMLElement} */          this._node = node; +        /** @type {import('environment').OperatingSystem} */          this._os = os; +        /** @type {EventListenerCollection} */          this._eventListeners = new EventListenerCollection(); +        /** @type {?KeyboardMouseInputField} */          this._inputField = null; +        /** @type {?HTMLSelectElement} */          this._actionSelect = null; +        /** @type {string} */          this._basePath = `inputs.hotkeys[${this._index}]`; +        /** @type {Intl.Collator} */          this._stringComparer = stringComparer; +        /** @type {?HTMLButtonElement} */          this._enabledButton = null; +        /** @type {?import('../../dom/popup-menu.js').PopupMenu} */          this._scopeMenu = null; +        /** @type {EventListenerCollection} */          this._scopeMenuEventListeners = new EventListenerCollection(); +        /** @type {?HTMLElement} */          this._argumentContainer = null; +        /** @type {?HTMLInputElement} */          this._argumentInput = null; +        /** @type {EventListenerCollection} */          this._argumentEventListeners = new EventListenerCollection();      } +    /** */      prepare() {          const node = this._node; -        const menuButton = node.querySelector('.hotkey-list-item-button'); -        const input = node.querySelector('.hotkey-list-item-input'); -        const action = node.querySelector('.hotkey-list-item-action'); -        const enabledToggle = node.querySelector('.hotkey-list-item-enabled'); -        const scopesButton = node.querySelector('.hotkey-list-item-scopes-button'); -        const enabledButton = node.querySelector('.hotkey-list-item-enabled-button'); +        const menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-button')); +        const input = /** @type {HTMLInputElement} */ (node.querySelector('.hotkey-list-item-input')); +        const action = /** @type {HTMLSelectElement} */ (node.querySelector('.hotkey-list-item-action')); +        const enabledToggle = /** @type {HTMLInputElement} */ (node.querySelector('.hotkey-list-item-enabled')); +        const scopesButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-scopes-button')); +        const enabledButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-enabled-button'));          this._actionSelect = action;          this._enabledButton = enabledButton; @@ -238,9 +313,10 @@ class KeyboardShortcutHotkeyEntry {          this._eventListeners.on(this._inputField, 'change', this._onInputFieldChange.bind(this));      } +    /** */      cleanup() {          this._eventListeners.removeAllEventListeners(); -        this._inputField.cleanup(); +        /** @type {KeyboardMouseInputField} */ (this._inputField).cleanup();          this._clearScopeMenu();          this._clearArgumentEventListeners();          if (this._node.parentNode !== null) { @@ -250,11 +326,14 @@ class KeyboardShortcutHotkeyEntry {      // Private +    /** +     * @param {import('popup-menu').MenuOpenEvent} e +     */      _onMenuOpen(e) {          const {action} = this._data;          const {menu} = e.detail; -        const resetArgument = menu.bodyNode.querySelector('.popup-menu-item[data-menu-action="resetArgument"]'); +        const resetArgument = /** @type {HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action="resetArgument"]'));          const details = this._parent.getActionDetails(action);          const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0; @@ -262,13 +341,16 @@ class KeyboardShortcutHotkeyEntry {          resetArgument.hidden = (typeof argumentDetails === 'undefined');      } +    /** +     * @param {import('popup-menu').MenuCloseEvent} e +     */      _onMenuClose(e) {          switch (e.detail.action) {              case 'delete':                  this._delete();                  break;              case 'clearInputs': -                this._inputField.clearInputs(); +                /** @type {KeyboardMouseInputField} */ (this._inputField).clearInputs();                  break;              case 'resetInput':                  this._resetInput(); @@ -279,10 +361,13 @@ class KeyboardShortcutHotkeyEntry {          }      } +    /** +     * @param {import('popup-menu').MenuOpenEvent} e +     */      _onScopesMenuOpen(e) {          const {menu} = e.detail;          const validScopes = this._getValidScopesForAction(this._data.action); -        if (validScopes.size === 0) { +        if (validScopes === null || validScopes.size === 0) {              menu.close();              return;          } @@ -291,6 +376,9 @@ class KeyboardShortcutHotkeyEntry {          this._updateDisplay(menu.containerNode); // Fix a animation issue due to changing checkbox values      } +    /** +     * @param {import('popup-menu').MenuCloseEvent} e +     */      _onScopesMenuClose(e) {          const {menu, action} = e.detail;          if (action === 'toggleScope') { @@ -302,24 +390,45 @@ class KeyboardShortcutHotkeyEntry {          }      } +    /** +     * @param {import('keyboard-mouse-input-field').ChangeEvent} details +     */      _onInputFieldChange({key, modifiers}) { -        this._setKeyAndModifiers(key, modifiers); +        /** @type {import('input').ModifierKey[]} */ +        const modifiers2 = []; +        for (const modifier of modifiers) { +            const modifier2 = DocumentUtil.normalizeModifierKey(modifier); +            if (modifier2 === null) { continue; } +            modifiers2.push(modifier2); +        } +        this._setKeyAndModifiers(key, modifiers2);      } +    /** +     * @param {MouseEvent} e +     */      _onScopeCheckboxChange(e) { -        const node = e.currentTarget; -        const {scope} = node.dataset; -        if (typeof scope !== 'string') { return; } +        const node = /** @type {HTMLInputElement} */ (e.currentTarget); +        const scope = this._normalizeScope(node.dataset.scope); +        if (scope === null) { return; }          this._setScopeEnabled(scope, node.checked);      } +    /** +     * @param {MouseEvent} e +     */      _onActionSelectChange(e) { -        const value = e.currentTarget.value; +        const node = /** @type {HTMLSelectElement} */ (e.currentTarget); +        const value = node.value;          this._setAction(value);      } +    /** +     * @param {string} template +     * @param {Event} e +     */      _onArgumentValueChange(template, e) { -        const node = e.currentTarget; +        const node = /** @type {HTMLInputElement} */ (e.currentTarget);          let value = this._getArgumentInputValue(node);          switch (template) {              case 'hotkey-argument-move-offset': @@ -329,10 +438,15 @@ class KeyboardShortcutHotkeyEntry {          this._setArgument(value);      } +    /** */      async _delete() {          this._parent.deleteEntry(this._index);      } +    /** +     * @param {?string} key +     * @param {import('input').ModifierKey[]} modifiers +     */      async _setKeyAndModifiers(key, modifiers) {          this._data.key = key;          this._data.modifiers = modifiers; @@ -350,6 +464,10 @@ class KeyboardShortcutHotkeyEntry {          ]);      } +    /** +     * @param {import('settings').InputsHotkeyScope} scope +     * @param {boolean} enabled +     */      async _setScopeEnabled(scope, enabled) {          const scopes = this._data.scopes;          const index = scopes.indexOf(scope); @@ -372,10 +490,15 @@ class KeyboardShortcutHotkeyEntry {          this._updateScopesButton();      } +    /** +     * @param {import('settings-modifications').Modification[]} targets +     * @returns {Promise<import('settings-controller').ModifyResult[]>} +     */      async _modifyProfileSettings(targets) {          return await this._parent.settingsController.modifyProfileSettings(targets);      } +    /** */      async _resetInput() {          const defaultHotkeys = await this._parent.getDefaultHotkeys();          const defaultValue = this._getDefaultKeyAndModifiers(defaultHotkeys, this._data.action); @@ -383,9 +506,10 @@ class KeyboardShortcutHotkeyEntry {          const {key, modifiers} = defaultValue;          await this._setKeyAndModifiers(key, modifiers); -        this._inputField.setInput(key, modifiers); +        /** @type {KeyboardMouseInputField} */ (this._inputField).setInput(key, modifiers);      } +    /** */      async _resetArgument() {          const {action} = this._data;          const details = this._parent.getActionDetails(action); @@ -395,6 +519,11 @@ class KeyboardShortcutHotkeyEntry {          await this._setArgument(argumentDefault);      } +    /** +     * @param {import('settings').InputsHotkeyOptions[]} defaultHotkeys +     * @param {string} action +     * @returns {?{modifiers: import('settings').InputsHotkeyModifier[], key: ?string}} +     */      _getDefaultKeyAndModifiers(defaultHotkeys, action) {          for (const {action: action2, key, modifiers} of defaultHotkeys) {              if (action2 !== action) { continue; } @@ -403,16 +532,18 @@ class KeyboardShortcutHotkeyEntry {          return null;      } +    /** +     * @param {string} value +     */      async _setAction(value) {          const validScopesOld = this._getValidScopesForAction(this._data.action);          const scopes = this._data.scopes;          let details = this._parent.getActionDetails(value); -        if (typeof details === 'undefined') { details = {}; } +        if (typeof details === 'undefined') { details = {scopes: new Set()}; } -        let validScopes = details.scopes; -        if (typeof validScopes === 'undefined') { validScopes = new Set(); } +        const validScopes = details.scopes;          const {argument: argumentDetails} = details;          let defaultArgument = typeof argumentDetails !== 'undefined' ? argumentDetails.default : ''; @@ -462,6 +593,9 @@ class KeyboardShortcutHotkeyEntry {          this._updateActionArgument();      } +    /** +     * @param {string} value +     */      async _setArgument(value) {          this._data.argument = value; @@ -479,16 +613,24 @@ class KeyboardShortcutHotkeyEntry {          }]);      } +    /** */      _updateScopesMenu() {          if (this._scopeMenu === null) { return; }          this._updateScopeMenuItems(this._scopeMenu);      } +    /** +     * @param {string} action +     * @returns {?Set<import('settings').InputsHotkeyScope>} +     */      _getValidScopesForAction(action) {          const details = this._parent.getActionDetails(action);          return typeof details !== 'undefined' ? details.scopes : null;      } +    /** +     * @param {import('../../dom/popup-menu.js').PopupMenu} menu +     */      _updateScopeMenuItems(menu) {          this._scopeMenuEventListeners.removeAllEventListeners(); @@ -496,14 +638,15 @@ class KeyboardShortcutHotkeyEntry {          const validScopes = this._getValidScopesForAction(this._data.action);          const bodyNode = menu.bodyNode; -        const menuItems = bodyNode.querySelectorAll('.popup-menu-item'); +        const menuItems = /** @type {NodeListOf<HTMLElement>} */ (bodyNode.querySelectorAll('.popup-menu-item'));          for (const menuItem of menuItems) {              if (menuItem.dataset.menuAction !== 'toggleScope') { continue; } -            const {scope} = menuItem.dataset; +            const scope = this._normalizeScope(menuItem.dataset.scope); +            if (scope === null) { continue; }              menuItem.hidden = !(validScopes === null || validScopes.has(scope)); -            const checkbox = menuItem.querySelector('.hotkey-scope-checkbox'); +            const checkbox = /** @type {HTMLInputElement} */ (menuItem.querySelector('.hotkey-scope-checkbox'));              if (checkbox !== null) {                  checkbox.checked = scopes.includes(scope);                  this._scopeMenuEventListeners.addEventListener(checkbox, 'change', this._onScopeCheckboxChange.bind(this), false); @@ -511,16 +654,23 @@ class KeyboardShortcutHotkeyEntry {          }      } +    /** */      _clearScopeMenu() {          this._scopeMenuEventListeners.removeAllEventListeners();          this._scopeMenu = null;      } +    /** */      _updateScopesButton() {          const {scopes} = this._data; -        this._enabledButton.dataset.scopeCount = `${scopes.length}`; +        if (this._enabledButton !== null) { +            this._enabledButton.dataset.scopeCount = `${scopes.length}`; +        }      } +    /** +     * @param {HTMLElement} node +     */      _updateDisplay(node) {          const {style} = node;          const {display} = style; @@ -529,49 +679,64 @@ class KeyboardShortcutHotkeyEntry {          style.display = display;      } +    /** */      _updateActionArgument() {          this._clearArgumentEventListeners();          const {action, argument} = this._data;          const details = this._parent.getActionDetails(action); -        const {argument: argumentDetails} = typeof details !== 'undefined' ? details : {}; +        const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0; -        this._argumentContainer.textContent = ''; +        if (this._argumentContainer !== null) { +            this._argumentContainer.textContent = ''; +        }          if (typeof argumentDetails !== 'undefined') {              const {template} = argumentDetails;              const node = this._parent.settingsController.instantiateTemplate(template);              const inputSelector = '.hotkey-argument-input'; -            const inputNode = node.matches(inputSelector) ? node : node.querySelector(inputSelector); +            const inputNode = /** @type {HTMLInputElement} */ (node.matches(inputSelector) ? node : node.querySelector(inputSelector));              if (inputNode !== null) {                  this._setArgumentInputValue(inputNode, argument);                  this._argumentInput = inputNode;                  this._updateArgumentInputValidity();                  this._argumentEventListeners.addEventListener(inputNode, 'change', this._onArgumentValueChange.bind(this, template), false);              } -            this._argumentContainer.appendChild(node); +            if (this._argumentContainer !== null) { +                this._argumentContainer.appendChild(node); +            }          }      } +    /** */      _clearArgumentEventListeners() {          this._argumentEventListeners.removeAllEventListeners();          this._argumentInput = null;      } +    /** +     * @param {HTMLInputElement} node +     * @returns {string} +     */      _getArgumentInputValue(node) {          return node.value;      } +    /** +     * @param {HTMLInputElement} node +     * @param {string} value +     */      _setArgumentInputValue(node, value) {          node.value = value;      } +    /** */      async _updateArgumentInputValidity() {          if (this._argumentInput === null) { return; }          let okay = true;          const {action, argument} = this._data;          const details = this._parent.getActionDetails(action); -        const {argument: argumentDetails} = typeof details !== 'undefined' ? details : {}; +        const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0;          if (typeof argumentDetails !== 'undefined') {              const {template} = argumentDetails; @@ -585,6 +750,10 @@ class KeyboardShortcutHotkeyEntry {          this._argumentInput.dataset.invalid = `${!okay}`;      } +    /** +     * @param {string} path +     * @returns {Promise<boolean>} +     */      async _isHotkeyArgumentSettingPathValid(path) {          if (path.length === 0) { return true; } @@ -601,4 +770,19 @@ class KeyboardShortcutHotkeyEntry {          }          return false;      } + +    /** +     * @param {string|undefined} value +     * @returns {?import('settings').InputsHotkeyScope} +     */ +    _normalizeScope(value) { +        switch (value) { +            case 'popup': +            case 'search': +            case 'web': +                return value; +            default: +                return null; +        } +    }  } |