From 95ad1ae1ef4a53802c12eab4c9b1545af0333aa1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 16 Dec 2023 19:55:02 -0500 Subject: Safer query selector (#364) * Add querySelectorNotNull helper function * Use querySelectorNotNull * Updates * Update settings * Remove unused * Update * Update function calls * More updates * Update types * Remove obsolete code --- ext/js/display/display-anki.js | 18 ++-- ext/js/display/display-audio.js | 18 ++-- ext/js/display/display-notification.js | 5 +- ext/js/display/display-profile-selection.js | 15 ++- ext/js/display/display.js | 40 ++++---- ext/js/display/query-parser.js | 7 +- ext/js/display/search-display-controller.js | 13 +-- ext/js/dom/popup-menu.js | 5 +- ext/js/dom/query-selector.js | 43 ++++++++ ext/js/pages/action-popup-main.js | 11 ++- ext/js/pages/info-main.js | 46 ++++++--- ext/js/pages/permissions-main.js | 12 ++- ext/js/pages/settings/anki-controller.js | 83 ++++++++-------- ext/js/pages/settings/anki-templates-controller.js | 36 +++---- ext/js/pages/settings/audio-controller.js | 40 ++++---- ext/js/pages/settings/backup-controller.js | 29 ++++-- .../settings/collapsible-dictionary-controller.js | 17 ++-- ext/js/pages/settings/dictionary-controller.js | 108 +++++++++++++-------- .../pages/settings/dictionary-import-controller.js | 58 +++-------- .../extension-keyboard-shortcuts-controller.js | 26 ++--- .../settings/keyboard-shortcuts-controller.js | 51 +++++----- ext/js/pages/settings/mecab-controller.js | 12 +-- ext/js/pages/settings/modal.js | 3 +- ext/js/pages/settings/nested-popups-controller.js | 17 ++-- .../settings/permissions-origin-controller.js | 30 +++--- .../settings/persistent-storage-controller.js | 12 ++- ext/js/pages/settings/popup-preview-controller.js | 23 ++--- ext/js/pages/settings/popup-preview-frame.js | 15 ++- ext/js/pages/settings/popup-window-controller.js | 4 +- ext/js/pages/settings/profile-conditions-ui.js | 41 ++++---- ext/js/pages/settings/profile-controller.js | 56 +++++------ ext/js/pages/settings/scan-inputs-controller.js | 45 ++++----- .../settings/scan-inputs-simple-controller.js | 12 +-- .../secondary-search-dictionary-controller.js | 16 +-- .../sentence-termination-characters-controller.js | 48 ++++----- .../pages/settings/settings-display-controller.js | 15 ++- ext/js/pages/settings/settings-main.js | 5 +- .../sort-frequency-dictionary-controller.js | 22 ++--- ext/js/pages/settings/status-footer.js | 6 +- ext/js/pages/settings/storage-controller.js | 4 +- .../translation-text-replacements-controller.js | 35 ++++--- ext/js/pages/welcome-main.js | 5 +- 42 files changed, 609 insertions(+), 498 deletions(-) create mode 100644 ext/js/dom/query-selector.js (limited to 'ext') diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 574e90ee..81791285 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -20,6 +20,7 @@ import {EventListenerCollection, deferPromise} from '../core.js'; import {AnkiNoteBuilder} from '../data/anki-note-builder.js'; import {AnkiUtil} from '../data/anki-util.js'; import {PopupMenu} from '../dom/popup-menu.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; export class DisplayAnki { @@ -91,7 +92,7 @@ export class DisplayAnki { ['term', ['term-kanji', 'term-kana']] ]); /** @type {HTMLElement} */ - this._menuContainer = /** @type {HTMLElement} */ (document.querySelector('#popup-menus')); + this._menuContainer = querySelectorNotNull(document, '#popup-menus'); /** @type {(event: MouseEvent) => void} */ this._onShowTagsBind = this._onShowTags.bind(this); /** @type {(event: MouseEvent) => void} */ @@ -827,7 +828,8 @@ export class DisplayAnki { button.hidden = disabled; button.dataset.noteIds = allNoteIds.join(' '); - const badge = /** @type {?HTMLElement} */ (button.querySelector('.action-button-badge')); + /** @type {?HTMLElement} */ + const badge = button.querySelector('.action-button-badge'); if (badge !== null) { const badgeData = badge.dataset; if (allNoteIds.length > 1) { @@ -866,13 +868,17 @@ export class DisplayAnki { const noteIds = this._getNodeNoteIds(node); if (noteIds.length === 0) { return; } - const menuContainerNode = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu')); - const menuBodyNode = /** @type {HTMLElement} */ (menuContainerNode.querySelector('.popup-menu-body')); + /** @type {HTMLElement} */ + const menuContainerNode = this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu'); + /** @type {HTMLElement} */ + const menuBodyNode = querySelectorNotNull(menuContainerNode, '.popup-menu-body'); for (let i = 0, ii = noteIds.length; i < ii; ++i) { const noteId = noteIds[i]; - const item = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu-item')); - const label = /** @type {Element} */ (item.querySelector('.popup-menu-item-label')); + /** @type {HTMLElement} */ + const item = this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu-item'); + /** @type {Element} */ + const label = querySelectorNotNull(item, '.popup-menu-item-label'); label.textContent = `Note ${i + 1}: ${noteId}`; item.dataset.menuAction = 'viewNote'; item.dataset.noteIds = `${noteId}`; diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index 3576decb..1f279030 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -18,6 +18,7 @@ import {EventListenerCollection} from '../core.js'; import {PopupMenu} from '../dom/popup-menu.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {AudioSystem} from '../media/audio-system.js'; import {yomitan} from '../yomitan.js'; @@ -45,7 +46,7 @@ export class DisplayAudio { /** @type {Map} */ this._cache = new Map(); /** @type {Element} */ - this._menuContainer = /** @type {Element} */ (document.querySelector('#popup-menus')); + this._menuContainer = querySelectorNotNull(document, '#popup-menus'); /** @type {import('core').TokenObject} */ this._entriesToken = {}; /** @type {Set} */ @@ -715,7 +716,8 @@ export class DisplayAudio { button.dataset.potentialAvailableAudioCount = `${potentialAvailableAudioCount}`; } - const badge = /** @type {?HTMLElement} */ (button.querySelector('.action-button-badge')); + /** @type {?HTMLElement} */ + const badge = button.querySelector('.action-button-badge'); if (badge === null) { return; } const badgeData = badge.dataset; @@ -804,7 +806,8 @@ export class DisplayAudio { _createMenu(sourceButton, term, reading) { // Create menu const menuContainerNode = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('audio-button-popup-menu')); - const menuBodyNode = /** @type {HTMLElement} */ (menuContainerNode.querySelector('.popup-menu-body')); + /** @type {HTMLElement} */ + const menuBodyNode = querySelectorNotNull(menuContainerNode, '.popup-menu-body'); menuContainerNode.dataset.term = term; menuContainerNode.dataset.reading = reading; @@ -837,7 +840,8 @@ export class DisplayAudio { const existingNode = this._getOrCreateMenuItem(currentItems, index, subIndex); const node = existingNode !== null ? existingNode : /** @type {HTMLElement} */ (displayGenerator.instantiateTemplate('audio-button-popup-menu-item')); - const labelNode = /** @type {HTMLElement} */ (node.querySelector('.popup-menu-item-audio-button .popup-menu-item-label')); + /** @type {HTMLElement} */ + const labelNode = querySelectorNotNull(node, '.popup-menu-item-audio-button .popup-menu-item-label'); let label = name; if (!nameUnique) { label = `${label} ${nameIndex + 1}`; @@ -847,11 +851,13 @@ export class DisplayAudio { if (typeof subName === 'string' && subName.length > 0) { label += `: ${subName}`; } labelNode.textContent = label; - const cardButton = /** @type {HTMLElement} */ (node.querySelector('.popup-menu-item-set-primary-audio-button')); + /** @type {HTMLElement} */ + const cardButton = querySelectorNotNull(node, '.popup-menu-item-set-primary-audio-button'); cardButton.hidden = !downloadable; if (valid !== null) { - const icon = /** @type {HTMLElement} */ (node.querySelector('.popup-menu-item-audio-button .popup-menu-item-icon')); + /** @type {HTMLElement} */ + const icon = querySelectorNotNull(node, '.popup-menu-item-audio-button .popup-menu-item-icon'); icon.dataset.icon = valid ? 'checkmark' : 'cross'; showIcons = true; } diff --git a/ext/js/display/display-notification.js b/ext/js/display/display-notification.js index d1cfa537..a0e9e35c 100644 --- a/ext/js/display/display-notification.js +++ b/ext/js/display/display-notification.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../core.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; export class DisplayNotification { /** @@ -29,9 +30,9 @@ export class DisplayNotification { /** @type {HTMLElement} */ this._node = node; /** @type {HTMLElement} */ - this._body = /** @type {HTMLElement} */ (node.querySelector('.footer-notification-body')); + this._body = querySelectorNotNull(node, '.footer-notification-body'); /** @type {HTMLElement} */ - this._closeButton = /** @type {HTMLElement} */ (node.querySelector('.footer-notification-close-button')); + this._closeButton = querySelectorNotNull(node, '.footer-notification-close-button'); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); /** @type {?import('core').Timeout} */ diff --git a/ext/js/display/display-profile-selection.js b/ext/js/display/display-profile-selection.js index c5cb7d06..f2ebd3e4 100644 --- a/ext/js/display/display-profile-selection.js +++ b/ext/js/display/display-profile-selection.js @@ -18,6 +18,7 @@ import {EventListenerCollection, generateId} from '../core.js'; import {PanelElement} from '../dom/panel-element.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; export class DisplayProfileSelection { @@ -28,12 +29,14 @@ export class DisplayProfileSelection { /** @type {import('./display.js').Display} */ this._display = display; /** @type {HTMLElement} */ - this._profielList = /** @type {HTMLElement} */ (document.querySelector('#profile-list')); + this._profielList = querySelectorNotNull(document, '#profile-list'); /** @type {HTMLButtonElement} */ - this._profileButton = /** @type {HTMLButtonElement} */ (document.querySelector('#profile-button')); + this._profileButton = querySelectorNotNull(document, '#profile-button'); + /** @type {HTMLElement} */ + const profilePanelElement = querySelectorNotNull(document, '#profile-panel'); /** @type {PanelElement} */ this._profilePanel = new PanelElement({ - node: /** @type {HTMLElement} */ (document.querySelector('#profile-panel')), + node: profilePanelElement, closingAnimationDuration: 375 // Milliseconds; includes buffer }); /** @type {boolean} */ @@ -98,9 +101,11 @@ export class DisplayProfileSelection { for (let i = 0, ii = profiles.length; i < ii; ++i) { const {name} = profiles[i]; const entry = displayGenerator.createProfileListItem(); - const radio = /** @type {HTMLInputElement} */ (entry.querySelector('.profile-entry-is-default-radio')); + /** @type {HTMLInputElement} */ + const radio = querySelectorNotNull(entry, '.profile-entry-is-default-radio'); radio.checked = (i === profileCurrent); - const nameNode = /** @type {Element} */ (entry.querySelector('.profile-list-item-name')); + /** @type {Element} */ + const nameNode = querySelectorNotNull(entry, '.profile-list-item-name'); nameNode.textContent = name; fragment.appendChild(entry); this._eventListeners.addEventListener(radio, 'change', this._onProfileRadioChange.bind(this, i), false); diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 9c2d97f2..b6a818ba 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -22,6 +22,7 @@ import {ThemeController} from '../app/theme-controller.js'; import {FrameEndpoint} from '../comm/frame-endpoint.js'; import {DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, invokeMessageHandler, log, promiseTimeout} from '../core.js'; import {PopupMenu} from '../dom/popup-menu.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {ScrollElement} from '../dom/scroll-element.js'; import {HotkeyHelpController} from '../input/hotkey-help-controller.js'; import {TextScanner} from '../language/text-scanner.js'; @@ -62,7 +63,7 @@ export class Display extends EventDispatcher { /** @type {import('../input/hotkey-handler.js').HotkeyHandler} */ this._hotkeyHandler = hotkeyHandler; /** @type {HTMLElement} */ - this._container = /** @type {HTMLElement} */ (document.querySelector('#dictionary-entries')); + this._container = querySelectorNotNull(document, '#dictionary-entries'); /** @type {import('dictionary').DictionaryEntry[]} */ this._dictionaryEntries = []; /** @type {HTMLElement[]} */ @@ -116,7 +117,7 @@ export class Display extends EventDispatcher { /** @type {number} */ this._queryOffset = 0; /** @type {HTMLElement} */ - this._progressIndicator = /** @type {HTMLElement} */ (document.querySelector('#progress-indicator')); + this._progressIndicator = querySelectorNotNull(document, '#progress-indicator'); /** @type {?import('core').Timeout} */ this._progressIndicatorTimer = null; /** @type {DynamicProperty} */ @@ -126,24 +127,24 @@ export class Display extends EventDispatcher { /** @type {?boolean} */ this._queryParserVisibleOverride = null; /** @type {HTMLElement} */ - this._queryParserContainer = /** @type {HTMLElement} */ (document.querySelector('#query-parser-container')); + this._queryParserContainer = querySelectorNotNull(document, '#query-parser-container'); /** @type {QueryParser} */ this._queryParser = new QueryParser({ getSearchContext: this._getSearchContext.bind(this), japaneseUtil }); /** @type {HTMLElement} */ - this._contentScrollElement = /** @type {HTMLElement} */ (document.querySelector('#content-scroll')); + this._contentScrollElement = querySelectorNotNull(document, '#content-scroll'); /** @type {HTMLElement} */ - this._contentScrollBodyElement = /** @type {HTMLElement} */ (document.querySelector('#content-body')); + this._contentScrollBodyElement = querySelectorNotNull(document, '#content-body'); /** @type {ScrollElement} */ this._windowScroll = new ScrollElement(this._contentScrollElement); - /** @type {HTMLButtonElement} */ - this._closeButton = /** @type {HTMLButtonElement} */ (document.querySelector('#close-button')); - /** @type {HTMLButtonElement} */ - this._navigationPreviousButton = /** @type {HTMLButtonElement} */ (document.querySelector('#navigate-previous-button')); - /** @type {HTMLButtonElement} */ - this._navigationNextButton = /** @type {HTMLButtonElement} */ (document.querySelector('#navigate-next-button')); + /** @type {?HTMLButtonElement} */ + this._closeButton = document.querySelector('#close-button'); + /** @type {?HTMLButtonElement} */ + this._navigationPreviousButton = document.querySelector('#navigate-previous-button'); + /** @type {?HTMLButtonElement} */ + this._navigationNextButton = document.querySelector('#navigate-next-button'); /** @type {?Frontend} */ this._frontend = null; /** @type {?Promise} */ @@ -171,7 +172,7 @@ export class Display extends EventDispatcher { /** @type {?import('./display-notification.js').DisplayNotification} */ this._tagNotification = null; /** @type {HTMLElement} */ - this._footerNotificationContainer = /** @type {HTMLElement} */ (document.querySelector('#content-footer')); + this._footerNotificationContainer = querySelectorNotNull(document, '#content-footer'); /** @type {OptionToggleHotkeyHandler} */ this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this); /** @type {ElementOverflowController} */ @@ -179,7 +180,7 @@ export class Display extends EventDispatcher { /** @type {boolean} */ this._frameVisible = (pageType === 'search'); /** @type {HTMLElement} */ - this._menuContainer = /** @type {HTMLElement} */ (document.querySelector('#popup-menus')); + this._menuContainer = querySelectorNotNull(document, '#popup-menus'); /** @type {(event: MouseEvent) => void} */ this._onEntryClickBind = this._onEntryClick.bind(this); /** @type {(event: MouseEvent) => void} */ @@ -1041,7 +1042,8 @@ export class Display extends EventDispatcher { const node = /** @type {HTMLElement} */ (e.currentTarget); const menuContainerNode = /** @type {HTMLElement} */ (this._displayGenerator.instantiateTemplate('dictionary-entry-popup-menu')); - const menuBodyNode = /** @type {HTMLElement} */ (menuContainerNode.querySelector('.popup-menu-body')); + /** @type {HTMLElement} */ + const menuBodyNode = querySelectorNotNull(menuContainerNode, '.popup-menu-body'); /** * @param {string} menuAction @@ -1049,7 +1051,9 @@ export class Display extends EventDispatcher { */ const addItem = (menuAction, label) => { const item = /** @type {HTMLElement} */ (this._displayGenerator.instantiateTemplate('dictionary-entry-popup-menu-item')); - /** @type {HTMLElement} */ (item.querySelector('.popup-menu-item-label')).textContent = label; + /** @type {HTMLElement} */ + const labelElement = querySelectorNotNull(item, '.popup-menu-item-label'); + labelElement.textContent = label; item.dataset.menuAction = menuAction; menuBodyNode.appendChild(item); }; @@ -1291,7 +1295,8 @@ export class Display extends EventDispatcher { /** */ _setContentExtensionUnloaded() { - const errorExtensionUnloaded = /** @type {?HTMLElement} */ (document.querySelector('#error-extension-unloaded')); + /** @type {?HTMLElement} */ + const errorExtensionUnloaded = document.querySelector('#error-extension-unloaded'); if (this._container !== null) { this._container.hidden = true; @@ -1323,7 +1328,8 @@ export class Display extends EventDispatcher { * @param {boolean} visible */ _setNoContentVisible(visible) { - const noResults = /** @type {?HTMLElement} */ (document.querySelector('#no-results')); + /** @type {?HTMLElement} */ + const noResults = document.querySelector('#no-results'); if (noResults !== null) { noResults.hidden = !visible; diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js index 03b54fd5..e71c7251 100644 --- a/ext/js/display/query-parser.js +++ b/ext/js/display/query-parser.js @@ -17,6 +17,7 @@ */ import {EventDispatcher, log} from '../core.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {TextScanner} from '../language/text-scanner.js'; import {yomitan} from '../yomitan.js'; @@ -50,11 +51,11 @@ export class QueryParser extends EventDispatcher { /** @type {import('api').ParseTextResult} */ this._parseResults = []; /** @type {HTMLElement} */ - this._queryParser = /** @type {HTMLElement} */ (document.querySelector('#query-parser-content')); + this._queryParser = querySelectorNotNull(document, '#query-parser-content'); /** @type {HTMLElement} */ - this._queryParserModeContainer = /** @type {HTMLElement} */ (document.querySelector('#query-parser-mode-container')); + this._queryParserModeContainer = querySelectorNotNull(document, '#query-parser-mode-container'); /** @type {HTMLSelectElement} */ - this._queryParserModeSelect = /** @type {HTMLSelectElement} */ (document.querySelector('#query-parser-mode-select')); + this._queryParserModeSelect = querySelectorNotNull(document, '#query-parser-mode-select'); /** @type {TextScanner} */ this._textScanner = new TextScanner({ node: this._queryParser, diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index 2778c4cd..b512a16d 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -19,6 +19,7 @@ import * as wanakana from '../../lib/wanakana.js'; import {ClipboardMonitor} from '../comm/clipboard-monitor.js'; import {EventListenerCollection, invokeMessageHandler} from '../core.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; export class SearchDisplayController { @@ -42,17 +43,17 @@ export class SearchDisplayController { /** @type {import('./search-persistent-state-controller.js').SearchPersistentStateController} */ this._searchPersistentStateController = searchPersistentStateController; /** @type {HTMLButtonElement} */ - this._searchButton = /** @type {HTMLButtonElement} */ (document.querySelector('#search-button')); + this._searchButton = querySelectorNotNull(document, '#search-button'); /** @type {HTMLButtonElement} */ - this._searchBackButton = /** @type {HTMLButtonElement} */ (document.querySelector('#search-back-button')); + this._searchBackButton = querySelectorNotNull(document, '#search-back-button'); /** @type {HTMLTextAreaElement} */ - this._queryInput = /** @type {HTMLTextAreaElement} */ (document.querySelector('#search-textbox')); + this._queryInput = querySelectorNotNull(document, '#search-textbox'); /** @type {HTMLElement} */ - this._introElement = /** @type {HTMLElement} */ (document.querySelector('#intro')); + this._introElement = querySelectorNotNull(document, '#intro'); /** @type {HTMLInputElement} */ - this._clipboardMonitorEnableCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#clipboard-monitor-enable')); + this._clipboardMonitorEnableCheckbox = querySelectorNotNull(document, '#clipboard-monitor-enable'); /** @type {HTMLInputElement} */ - this._wanakanaEnableCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#wanakana-enable')); + this._wanakanaEnableCheckbox = querySelectorNotNull(document, '#wanakana-enable'); /** @type {EventListenerCollection} */ this._queryInputEvents = new EventListenerCollection(); /** @type {boolean} */ diff --git a/ext/js/dom/popup-menu.js b/ext/js/dom/popup-menu.js index 78394c93..33cdd1ae 100644 --- a/ext/js/dom/popup-menu.js +++ b/ext/js/dom/popup-menu.js @@ -17,6 +17,7 @@ */ import {EventDispatcher, EventListenerCollection} from '../core.js'; +import {querySelectorNotNull} from './query-selector.js'; /** * @augments EventDispatcher @@ -33,9 +34,9 @@ export class PopupMenu extends EventDispatcher { /** @type {HTMLElement} */ this._containerNode = containerNode; /** @type {HTMLElement} */ - this._node = /** @type {HTMLElement} */ (containerNode.querySelector('.popup-menu')); + this._node = querySelectorNotNull(containerNode, '.popup-menu'); /** @type {HTMLElement} */ - this._bodyNode = /** @type {HTMLElement} */ (containerNode.querySelector('.popup-menu-body')); + this._bodyNode = querySelectorNotNull(containerNode, '.popup-menu-body'); /** @type {boolean} */ this._isClosed = false; /** @type {EventListenerCollection} */ diff --git a/ext/js/dom/query-selector.js b/ext/js/dom/query-selector.js new file mode 100644 index 00000000..e881211d --- /dev/null +++ b/ext/js/dom/query-selector.js @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import {ExtensionError} from '../core/extension-error.js'; + +/** + * @param {Element|Document|DocumentFragment} element + * @param {string} selector + * @returns {ExtensionError} + */ +function createError(element, selector) { + const error = new ExtensionError(`Performing querySelectorNotNull(element, ${JSON.stringify(selector)}) returned null`); + error.data = {element, selector}; + return error; +} + +/** + * @template {Element} T + * @param {Element|Document|DocumentFragment} element + * @param {string} selector + * @returns {T} + * @throws {Error} + */ +export function querySelectorNotNull(element, selector) { + /** @type {?T} */ + const result = element.querySelector(selector); + if (result === null) { throw createError(element, selector); } + return result; +} diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js index 94b9b356..f8dd865f 100644 --- a/ext/js/pages/action-popup-main.js +++ b/ext/js/pages/action-popup-main.js @@ -17,6 +17,7 @@ */ import {PermissionsUtil} from '../data/permissions-util.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {HotkeyHelpController} from '../input/hotkey-help-controller.js'; import {yomitan} from '../yomitan.js'; @@ -57,7 +58,9 @@ export class DisplayController { this._setupOptions(primaryProfile); } - /** @type {HTMLElement} */ (document.querySelector('.action-select-profile')).hidden = (profiles.length <= 1); + /** @type {HTMLElement} */ + const profileSelect = querySelectorNotNull(document, '.action-select-profile'); + profileSelect.hidden = (profiles.length <= 1); this._updateProfileSelect(profiles, profileCurrent); @@ -207,8 +210,10 @@ export class DisplayController { * @param {number} profileCurrent */ _updateProfileSelect(profiles, profileCurrent) { - const select = /** @type {HTMLSelectElement} */ (document.querySelector('#profile-select')); - const optionGroup = /** @type {HTMLElement} */ (document.querySelector('#profile-select-option-group')); + /** @type {HTMLSelectElement} */ + const select = querySelectorNotNull(document, '#profile-select'); + /** @type {HTMLElement} */ + const optionGroup = querySelectorNotNull(document, '#profile-select-option-group'); const fragment = document.createDocumentFragment(); for (let i = 0, ii = profiles.length; i < ii; ++i) { const {name} = profiles[i]; diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js index f71d64c3..7445354f 100644 --- a/ext/js/pages/info-main.js +++ b/ext/js/pages/info-main.js @@ -18,6 +18,7 @@ import {log, promiseTimeout} from '../core.js'; import {DocumentFocusController} from '../dom/document-focus-controller.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; import {BackupController} from './settings/backup-controller.js'; import {SettingsController} from './settings/settings-controller.js'; @@ -69,15 +70,27 @@ function getOperatingSystemDisplayName(os) { const {name, version} = manifest; const {browser, platform: {os}} = await yomitan.api.getEnvironmentInfo(); - const thisVersionLink = /** @type {HTMLLinkElement} */ (document.querySelector('#release-notes-this-version-link')); + /** @type {HTMLLinkElement} */ + const thisVersionLink = querySelectorNotNull(document, '#release-notes-this-version-link'); const {hrefFormat} = thisVersionLink.dataset; thisVersionLink.href = typeof hrefFormat === 'string' ? hrefFormat.replace(/\{version\}/g, version) : ''; - /** @type {HTMLElement} */ (document.querySelector('#version')).textContent = `${name} ${version}`; - /** @type {HTMLElement} */ (document.querySelector('#browser')).textContent = getBrowserDisplayName(browser); - /** @type {HTMLElement} */ (document.querySelector('#platform')).textContent = getOperatingSystemDisplayName(os); - /** @type {HTMLElement} */ (document.querySelector('#language')).textContent = `${language}`; - /** @type {HTMLElement} */ (document.querySelector('#user-agent')).textContent = userAgent; + /** @type {HTMLElement} */ + const versionElement = querySelectorNotNull(document, '#version'); + /** @type {HTMLElement} */ + const browserElement = querySelectorNotNull(document, '#browser'); + /** @type {HTMLElement} */ + const platformElement = querySelectorNotNull(document, '#platform'); + /** @type {HTMLElement} */ + const languageElement = querySelectorNotNull(document, '#language'); + /** @type {HTMLElement} */ + const userAgentElement = querySelectorNotNull(document, '#user-agent'); + + versionElement.textContent = `${name} ${version}`; + browserElement.textContent = getBrowserDisplayName(browser); + platformElement.textContent = getOperatingSystemDisplayName(os); + languageElement.textContent = `${language}`; + userAgentElement.textContent = userAgent; (async () => { let ankiConnectVersion = null; @@ -87,9 +100,16 @@ function getOperatingSystemDisplayName(os) { // NOP } - /** @type {HTMLElement} */ (document.querySelector('#anki-connect-version')).textContent = (ankiConnectVersion !== null ? `${ankiConnectVersion}` : 'Unknown'); - /** @type {HTMLElement} */ (document.querySelector('#anki-connect-version-container')).dataset.hasError = `${ankiConnectVersion === null}`; - /** @type {HTMLElement} */ (document.querySelector('#anki-connect-version-unknown-message')).hidden = (ankiConnectVersion !== null); + /** @type {HTMLElement} */ + const ankiVersionElement = querySelectorNotNull(document, '#anki-connect-version'); + /** @type {HTMLElement} */ + const ankiVersionContainerElement = querySelectorNotNull(document, '#anki-connect-version-container'); + /** @type {HTMLElement} */ + const ankiVersionUnknownElement = querySelectorNotNull(document, '#anki-connect-version-unknown-message'); + + ankiVersionElement.textContent = (ankiConnectVersion !== null ? `${ankiConnectVersion}` : 'Unknown'); + ankiVersionContainerElement.dataset.hasError = `${ankiConnectVersion === null}`; + ankiVersionUnknownElement.hidden = (ankiConnectVersion !== null); })(); (async () => { @@ -115,8 +135,12 @@ function getOperatingSystemDisplayName(os) { fragment.appendChild(node); } - /** @type {HTMLElement} */ (document.querySelector('#installed-dictionaries-none')).hidden = (dictionaryInfos.length !== 0); - const container = /** @type {HTMLElement} */ (document.querySelector('#installed-dictionaries')); + /** @type {HTMLElement} */ + const noneElement = querySelectorNotNull(document, '#installed-dictionaries-none'); + + noneElement.hidden = (dictionaryInfos.length !== 0); + /** @type {HTMLElement} */ + const container = querySelectorNotNull(document, '#installed-dictionaries'); container.textContent = ''; container.appendChild(fragment); })(); diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js index 064e9240..58dae310 100644 --- a/ext/js/pages/permissions-main.js +++ b/ext/js/pages/permissions-main.js @@ -18,6 +18,7 @@ import {log, promiseTimeout} from '../core.js'; import {DocumentFocusController} from '../dom/document-focus-controller.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; import {ExtensionContentController} from './common/extension-content-controller.js'; import {ModalController} from './settings/modal-controller.js'; @@ -99,11 +100,12 @@ function setupPermissionsToggles() { setupEnvironmentInfo(); - /** @type {[HTMLInputElement, HTMLInputElement]} */ - const permissionsCheckboxes = [ - /** @type {HTMLInputElement} */ (document.querySelector('#permission-checkbox-allow-in-private-windows')), - /** @type {HTMLInputElement} */ (document.querySelector('#permission-checkbox-allow-file-url-access')) - ]; + /** @type {HTMLInputElement} */ + const permissionCheckbox1 = querySelectorNotNull(document, '#permission-checkbox-allow-in-private-windows'); + /** @type {HTMLInputElement} */ + const permissionCheckbox2 = querySelectorNotNull(document, '#permission-checkbox-allow-file-url-access'); + /** @type {HTMLInputElement[]} */ + const permissionsCheckboxes = [permissionCheckbox1, permissionCheckbox2]; const permissions = await Promise.all([ isAllowedIncognitoAccess(), diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index 737cc04a..f470d9fa 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -20,6 +20,7 @@ import {AnkiConnect} from '../../comm/anki-connect.js'; import {EventListenerCollection, log} from '../../core.js'; import {ExtensionError} from '../../core/extension-error.js'; import {AnkiUtil} from '../../data/anki-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {SelectorObserver} from '../../dom/selector-observer.js'; import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js'; import {yomitan} from '../../yomitan.js'; @@ -45,26 +46,27 @@ export class AnkiController { this._stringComparer = new Intl.Collator(); // Locale does not matter /** @type {?Promise} */ this._getAnkiDataPromise = null; - /** @type {?HTMLElement} */ - this._ankiErrorContainer = null; - /** @type {?HTMLElement} */ - this._ankiErrorMessageNode = null; + /** @type {HTMLElement} */ + this._ankiErrorMessageNode = querySelectorNotNull(document, '#anki-error-message'); + const ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent; /** @type {string} */ - this._ankiErrorMessageNodeDefaultContent = ''; - /** @type {?HTMLElement} */ - this._ankiErrorMessageDetailsNode = null; - /** @type {?HTMLElement} */ - this._ankiErrorMessageDetailsContainer = null; - /** @type {?HTMLElement} */ - this._ankiErrorMessageDetailsToggle = null; - /** @type {?HTMLElement} */ - this._ankiErrorInvalidResponseInfo = null; - /** @type {?HTMLElement} */ - this._ankiCardPrimary = null; + this._ankiErrorMessageNodeDefaultContent = typeof ankiErrorMessageNodeDefaultContent === 'string' ? ankiErrorMessageNodeDefaultContent : ''; + /** @type {HTMLElement} */ + this._ankiErrorMessageDetailsNode = querySelectorNotNull(document, '#anki-error-message-details'); + /** @type {HTMLElement} */ + this._ankiErrorMessageDetailsContainer = querySelectorNotNull(document, '#anki-error-message-details-container'); + /** @type {HTMLElement} */ + this._ankiErrorMessageDetailsToggle = querySelectorNotNull(document, '#anki-error-message-details-toggle'); + /** @type {HTMLElement} */ + this._ankiErrorInvalidResponseInfo = querySelectorNotNull(document, '#anki-error-invalid-response-info'); + /** @type {HTMLElement} */ + this._ankiCardPrimary = querySelectorNotNull(document, '#anki-card-primary'); /** @type {?Error} */ this._ankiError = null; /** @type {?import('core').TokenObject} */ this._validateFieldsToken = null; + /** @type {?HTMLInputElement} */ + this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]'); } /** @type {import('./settings-controller.js').SettingsController} */ @@ -74,19 +76,11 @@ export class AnkiController { /** */ async prepare() { - this._ankiErrorContainer = /** @type {HTMLElement} */ (document.querySelector('#anki-error')); - this._ankiErrorMessageNode = /** @type {HTMLElement} */ (document.querySelector('#anki-error-message')); - const ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent; - this._ankiErrorMessageNodeDefaultContent = typeof ankiErrorMessageNodeDefaultContent === 'string' ? ankiErrorMessageNodeDefaultContent : ''; - this._ankiErrorMessageDetailsNode = /** @type {HTMLElement} */ (document.querySelector('#anki-error-message-details')); - this._ankiErrorMessageDetailsContainer = /** @type {HTMLElement} */ (document.querySelector('#anki-error-message-details-container')); - this._ankiErrorMessageDetailsToggle = /** @type {HTMLElement} */ (document.querySelector('#anki-error-message-details-toggle')); - this._ankiErrorInvalidResponseInfo = /** @type {HTMLElement} */ (document.querySelector('#anki-error-invalid-response-info')); - this._ankiEnableCheckbox = /** @type {?HTMLInputElement} */ (document.querySelector('[data-setting="anki.enable"]')); - this._ankiCardPrimary = /** @type {HTMLElement} */ (document.querySelector('#anki-card-primary')); - const ankiApiKeyInput = /** @type {HTMLElement} */ (document.querySelector('#anki-api-key-input')); + /** @type {HTMLElement} */ + const ankiApiKeyInput = querySelectorNotNull(document, '#anki-api-key-input'); const ankiCardPrimaryTypeRadios = /** @type {NodeListOf} */ (document.querySelectorAll('input[type=radio][name=anki-card-primary-type]')); - const ankiErrorLog = /** @type {HTMLElement} */ (document.querySelector('#anki-error-log')); + /** @type {HTMLElement} */ + const ankiErrorLog = querySelectorNotNull(document, '#anki-error-log'); this._setupFieldMenus(); @@ -439,9 +433,6 @@ export class AnkiController { /** */ _hideAnkiError() { const ankiErrorMessageNode = /** @type {HTMLElement} */ (this._ankiErrorMessageNode); - if (this._ankiErrorContainer !== null) { - this._ankiErrorContainer.hidden = true; - } /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsContainer).hidden = true; /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsToggle).hidden = true; /** @type {HTMLElement} */ (this._ankiErrorInvalidResponseInfo).hidden = true; @@ -472,9 +463,6 @@ export class AnkiController { details += `${error.stack}`.trimRight(); /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsNode).textContent = details; - if (this._ankiErrorContainer !== null) { - this._ankiErrorContainer.hidden = false; - } /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsContainer).hidden = true; /** @type {HTMLElement} */ (this._ankiErrorInvalidResponseInfo).hidden = (errorString.indexOf('Invalid response') < 0); /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsToggle).hidden = false; @@ -534,7 +522,8 @@ export class AnkiController { * @param {?Error} error */ _setAnkiNoteViewerStatus(visible, error) { - const node = /** @type {HTMLElement} */ (document.querySelector('#test-anki-note-viewer-results')); + /** @type {HTMLElement} */ + const node = querySelectorNotNull(document, '#test-anki-note-viewer-results'); if (visible) { const success = (error === null); node.textContent = success ? 'Success!' : error.message; @@ -608,8 +597,12 @@ class AnkiCardController { const cardOptions = this._getCardOptions(ankiOptions, this._cardType); if (cardOptions === null) { return; } const {deck, model, fields} = cardOptions; - this._deckController.prepare(/** @type {HTMLSelectElement} */ (this._node.querySelector('.anki-card-deck')), deck); - this._modelController.prepare(/** @type {HTMLSelectElement} */ (this._node.querySelector('.anki-card-model')), model); + /** @type {HTMLSelectElement} */ + const deckControllerSelect = querySelectorNotNull(this._node, '.anki-card-deck'); + /** @type {HTMLSelectElement} */ + const modelControllerSelect = querySelectorNotNull(this._node, '.anki-card-model'); + this._deckController.prepare(deckControllerSelect, deck); + this._modelController.prepare(modelControllerSelect, model); this._fields = fields; this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields'); @@ -752,7 +745,8 @@ class AnkiCardController { _setFieldMarker(element, marker) { const container = element.closest('.anki-card-field-value-container'); if (container === null) { return; } - const input = /** @type {HTMLInputElement} */ (container.querySelector('.anki-card-field-value')); + /** @type {HTMLInputElement} */ + const input = querySelectorNotNull(container, '.anki-card-field-value'); input.value = `{${marker}}`; input.dispatchEvent(new Event('change')); } @@ -780,15 +774,19 @@ class AnkiCardController { for (const [fieldName, fieldValue] of Object.entries(this._fields)) { const content = this._settingsController.instantiateTemplateFragment('anki-card-field'); - const fieldNameContainerNode = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-name-container')); + /** @type {HTMLElement} */ + const fieldNameContainerNode = querySelectorNotNull(content, '.anki-card-field-name-container'); fieldNameContainerNode.dataset.index = `${index}`; - const fieldNameNode = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-name')); + /** @type {HTMLElement} */ + const fieldNameNode = querySelectorNotNull(content, '.anki-card-field-name'); fieldNameNode.textContent = fieldName; - const valueContainer = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-value-container')); + /** @type {HTMLElement} */ + const valueContainer = querySelectorNotNull(content, '.anki-card-field-value-container'); valueContainer.dataset.index = `${index}`; - const inputField = /** @type {HTMLInputElement} */ (content.querySelector('.anki-card-field-value')); + /** @type {HTMLInputElement} */ + const inputField = querySelectorNotNull(content, '.anki-card-field-value'); inputField.value = fieldValue; inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]); this._validateFieldPermissions(inputField, index, false); @@ -798,7 +796,8 @@ class AnkiCardController { this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false); this._validateField(inputField, index); - const menuButton = /** @type {?HTMLElement} */ (content.querySelector('.anki-card-field-value-menu-button')); + /** @type {?HTMLElement} */ + const menuButton = content.querySelector('.anki-card-field-value-menu-button'); if (menuButton !== null) { if (typeof this._cardMenu !== 'undefined') { menuButton.dataset.menu = this._cardMenu; diff --git a/ext/js/pages/settings/anki-templates-controller.js b/ext/js/pages/settings/anki-templates-controller.js index 89848ef3..03fda9ca 100644 --- a/ext/js/pages/settings/anki-templates-controller.js +++ b/ext/js/pages/settings/anki-templates-controller.js @@ -18,6 +18,7 @@ import {ExtensionError} from '../../core/extension-error.js'; import {AnkiNoteBuilder} from '../../data/anki-note-builder.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {JapaneseUtil} from '../../language/sandbox/japanese-util.js'; import {yomitan} from '../../yomitan.js'; @@ -40,14 +41,16 @@ export class AnkiTemplatesController { this._cachedDictionaryEntryText = null; /** @type {?string} */ this._defaultFieldTemplates = null; - /** @type {?HTMLTextAreaElement} */ - this._fieldTemplatesTextarea = null; - /** @type {?HTMLElement} */ - this._compileResultInfo = null; - /** @type {?HTMLInputElement} */ - this._renderFieldInput = null; - /** @type {?HTMLElement} */ - this._renderResult = null; + /** @type {HTMLTextAreaElement} */ + this._fieldTemplatesTextarea = querySelectorNotNull(document, '#anki-card-templates-textarea'); + /** @type {HTMLElement} */ + this._compileResultInfo = querySelectorNotNull(document, '#anki-card-templates-compile-result'); + /** @type {HTMLInputElement} */ + this._renderFieldInput = querySelectorNotNull(document, '#anki-card-templates-test-field-input'); + /** @type {HTMLInputElement} */ + this._renderTextInput = querySelectorNotNull(document, '#anki-card-templates-test-text-input'); + /** @type {HTMLElement} */ + this._renderResult = querySelectorNotNull(document, '#anki-card-templates-render-result'); /** @type {?import('./modal.js').Modal} */ this._fieldTemplateResetModal = null; /** @type {AnkiNoteBuilder} */ @@ -58,15 +61,14 @@ export class AnkiTemplatesController { async prepare() { this._defaultFieldTemplates = await yomitan.api.getDefaultAnkiFieldTemplates(); - this._fieldTemplatesTextarea = /** @type {HTMLTextAreaElement} */ (document.querySelector('#anki-card-templates-textarea')); - this._compileResultInfo = /** @type {HTMLElement} */ (document.querySelector('#anki-card-templates-compile-result')); - this._renderFieldInput = /** @type {HTMLInputElement} */ (document.querySelector('#anki-card-templates-test-field-input')); - this._renderTextInput = /** @type {HTMLInputElement} */ (document.querySelector('#anki-card-templates-test-text-input')); - this._renderResult = /** @type {HTMLElement} */ (document.querySelector('#anki-card-templates-render-result')); - const menuButton = /** @type {HTMLButtonElement} */ (document.querySelector('#anki-card-templates-test-field-menu-button')); - const testRenderButton = /** @type {HTMLButtonElement} */ (document.querySelector('#anki-card-templates-test-render-button')); - const resetButton = /** @type {HTMLButtonElement} */ (document.querySelector('#anki-card-templates-reset-button')); - const resetConfirmButton = /** @type {HTMLButtonElement} */ (document.querySelector('#anki-card-templates-reset-button-confirm')); + /** @type {HTMLButtonElement} */ + const menuButton = querySelectorNotNull(document, '#anki-card-templates-test-field-menu-button'); + /** @type {HTMLButtonElement} */ + const testRenderButton = querySelectorNotNull(document, '#anki-card-templates-test-render-button'); + /** @type {HTMLButtonElement} */ + const resetButton = querySelectorNotNull(document, '#anki-card-templates-reset-button'); + /** @type {HTMLButtonElement} */ + const resetConfirmButton = querySelectorNotNull(document, '#anki-card-templates-reset-button-confirm'); this._fieldTemplateResetModal = this._modalController.getModal('anki-card-templates-reset'); this._fieldTemplatesTextarea.addEventListener('change', this._onChanged.bind(this), false); diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js index 0a3f9454..af05ee70 100644 --- a/ext/js/pages/settings/audio-controller.js +++ b/ext/js/pages/settings/audio-controller.js @@ -17,6 +17,7 @@ */ import {EventDispatcher, EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {AudioSystem} from '../../media/audio-system.js'; /** @@ -35,14 +36,14 @@ export class AudioController extends EventDispatcher { this._modalController = modalController; /** @type {AudioSystem} */ this._audioSystem = new AudioSystem(); - /** @type {?HTMLElement} */ - this._audioSourceContainer = null; - /** @type {?HTMLButtonElement} */ - this._audioSourceAddButton = null; + /** @type {HTMLElement} */ + this._audioSourceContainer = querySelectorNotNull(document, '#audio-source-list'); + /** @type {HTMLButtonElement} */ + this._audioSourceAddButton = querySelectorNotNull(document, '#audio-source-add'); /** @type {AudioSourceEntry[]} */ this._audioSourceEntries = []; - /** @type {?HTMLInputElement} */ - this._voiceTestTextInput = null; + /** @type {HTMLInputElement} */ + this._voiceTestTextInput = querySelectorNotNull(document, '#text-to-speech-voice-test-text'); /** @type {import('audio-controller').VoiceInfo[]} */ this._voices = []; } @@ -61,11 +62,9 @@ export class AudioController extends EventDispatcher { async prepare() { this._audioSystem.prepare(); - this._voiceTestTextInput = /** @type {HTMLInputElement} */ (document.querySelector('#text-to-speech-voice-test-text')); - this._audioSourceContainer = /** @type {HTMLElement} */ (document.querySelector('#audio-source-list')); - this._audioSourceAddButton = /** @type {HTMLButtonElement} */ (document.querySelector('#audio-source-add')); this._audioSourceContainer.textContent = ''; - const testButton = /** @type {HTMLButtonElement} */ (document.querySelector('#text-to-speech-voice-test')); + /** @type {HTMLButtonElement} */ + const testButton = querySelectorNotNull(document, '#text-to-speech-voice-test'); this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); @@ -270,12 +269,12 @@ class AudioSourceEntry { this._node = node; /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); - /** @type {?HTMLSelectElement} */ - this._typeSelect = null; - /** @type {?HTMLInputElement} */ - this._urlInput = null; - /** @type {?HTMLSelectElement} */ - this._voiceSelect = null; + /** @type {HTMLSelectElement} */ + this._typeSelect = querySelectorNotNull(this._node, '.audio-source-type-select'); + /** @type {HTMLInputElement} */ + this._urlInput = querySelectorNotNull(this._node, '.audio-source-parameter-container[data-field=url] .audio-source-parameter'); + /** @type {HTMLSelectElement} */ + this._voiceSelect = querySelectorNotNull(this._node, '.audio-source-parameter-container[data-field=voice] .audio-source-parameter'); } /** @type {number} */ @@ -296,10 +295,8 @@ class AudioSourceEntry { prepare() { this._updateTypeParameter(); - const menuButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.audio-source-menu-button')); - this._typeSelect = /** @type {HTMLSelectElement} */ (this._node.querySelector('.audio-source-type-select')); - this._urlInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.audio-source-parameter-container[data-field=url] .audio-source-parameter')); - this._voiceSelect = /** @type {HTMLSelectElement} */ (this._node.querySelector('.audio-source-parameter-container[data-field=voice] .audio-source-parameter')); + /** @type {HTMLButtonElement} */ + const menuButton = querySelectorNotNull(this._node, '.audio-source-menu-button'); this._typeSelect.value = this._type; this._urlInput.value = this._url; @@ -389,7 +386,8 @@ class AudioSourceEntry { break; } - const helpNode = /** @type {?HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]')); + /** @type {?HTMLElement} */ + const helpNode = menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]'); if (helpNode !== null) { helpNode.hidden = !hasHelp; } diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 95433d1c..94f85416 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -20,6 +20,7 @@ import {Dexie} from '../../../lib/dexie.js'; import {isObject, log} from '../../core.js'; import {OptionsUtil} from '../../data/options-util.js'; import {ArrayBufferUtil} from '../../data/sandbox/array-buffer-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; import {DictionaryController} from './dictionary-controller.js'; @@ -243,7 +244,8 @@ export class BackupController { */ _showSettingsImportError(error) { log.error(error); - const element = /** @type {HTMLElement} */ (document.querySelector('#settings-import-error-message')); + /** @type {HTMLElement} */ + const element = querySelectorNotNull(document, '#settings-import-error-message'); element.textContent = `${error}`; if (this._settingsImportErrorModal !== null) { this._settingsImportErrorModal.setVisible(true); @@ -480,7 +482,8 @@ export class BackupController { /** */ _onSettingsImportClick() { - const element = /** @type {HTMLElement} */ (document.querySelector('#settings-import-file')); + /** @type {HTMLElement} */ + const element = querySelectorNotNull(document, '#settings-import-file'); element.click(); } @@ -538,7 +541,8 @@ export class BackupController { * @param {boolean} [isWarning] */ _databaseExportImportErrorMessage(message, isWarning=false) { - const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); + /** @type {HTMLElement} */ + const errorMessageContainer = querySelectorNotNull(document, '#db-ops-error-report'); errorMessageContainer.style.display = 'block'; errorMessageContainer.textContent = message; @@ -557,7 +561,8 @@ export class BackupController { _databaseExportProgressCallback({totalRows, completedRows, done}) { // eslint-disable-next-line no-console console.log(`Progress: ${completedRows} of ${totalRows} rows completed`); - const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); + /** @type {HTMLElement} */ + const messageContainer = querySelectorNotNull(document, '#db-ops-progress-report'); messageContainer.style.display = 'block'; messageContainer.textContent = `Export Progress: ${completedRows} of ${totalRows} rows completed`; @@ -589,7 +594,8 @@ export class BackupController { return; } - const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); + /** @type {HTMLElement} */ + const errorMessageContainer = querySelectorNotNull(document, '#db-ops-error-report'); errorMessageContainer.style.display = 'none'; const date = new Date(Date.now()); @@ -619,7 +625,8 @@ export class BackupController { _databaseImportProgressCallback({totalRows, completedRows, done}) { // eslint-disable-next-line no-console console.log(`Progress: ${completedRows} of ${totalRows} rows completed`); - const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); + /** @type {HTMLElement} */ + const messageContainer = querySelectorNotNull(document, '#db-ops-progress-report'); messageContainer.style.display = 'block'; messageContainer.style.color = '#4169e1'; messageContainer.textContent = `Import Progress: ${completedRows} of ${totalRows} rows completed`; @@ -645,7 +652,9 @@ export class BackupController { /** */ _onSettingsImportDatabaseClick() { - /** @type {HTMLElement} */ (document.querySelector('#settings-import-db')).click(); + /** @type {HTMLElement} */ + const element = querySelectorNotNull(document, '#settings-import-db'); + element.click(); } /** @@ -658,7 +667,8 @@ export class BackupController { return; } - const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); + /** @type {HTMLElement} */ + const errorMessageContainer = querySelectorNotNull(document, '#db-ops-error-report'); errorMessageContainer.style.display = 'none'; const element = /** @type {HTMLInputElement} */ (e.currentTarget); @@ -675,7 +685,8 @@ export class BackupController { } catch (error) { // eslint-disable-next-line no-console console.log(error); - const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); + /** @type {HTMLElement} */ + const messageContainer = querySelectorNotNull(document, '#db-ops-progress-report'); messageContainer.style.color = 'red'; this._databaseExportImportErrorMessage('Encountered errors when importing. Please restart the browser and try again. If it continues to fail, reinstall Yomitan and import dictionaries one-by-one.'); } finally { diff --git a/ext/js/pages/settings/collapsible-dictionary-controller.js b/ext/js/pages/settings/collapsible-dictionary-controller.js index 355292ad..cff3ad20 100644 --- a/ext/js/pages/settings/collapsible-dictionary-controller.js +++ b/ext/js/pages/settings/collapsible-dictionary-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class CollapsibleDictionaryController { @@ -32,8 +33,8 @@ export class CollapsibleDictionaryController { this._dictionaryInfoMap = new Map(); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); - /** @type {?HTMLElement} */ - this._container = null; + /** @type {HTMLElement} */ + this._container = querySelectorNotNull(document, '#collapsible-dictionary-list'); /** @type {HTMLSelectElement[]} */ this._selects = []; /** @type {?HTMLSelectElement} */ @@ -42,8 +43,6 @@ export class CollapsibleDictionaryController { /** */ async prepare() { - this._container = /** @type {HTMLElement} */ (document.querySelector('#collapsible-dictionary-list')); - await this._onDatabaseUpdated(); yomitan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); @@ -150,13 +149,17 @@ export class CollapsibleDictionaryController { const node = this._settingsController.instantiateTemplate('collapsible-dictionary-item'); fragment.appendChild(node); - const nameNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-title')); + /** @type {HTMLElement} */ + const nameNode = querySelectorNotNull(node, '.dictionary-title'); nameNode.textContent = dictionary; - const versionNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-version')); + /** @type {HTMLElement} */ + const versionNode = querySelectorNotNull(node, '.dictionary-version'); versionNode.textContent = version; - return /** @type {HTMLSelectElement} */ (node.querySelector('.definitions-collapsible')); + /** @type {HTMLSelectElement} */ + const select = querySelectorNotNull(node, '.definitions-collapsible'); + return select; } /** */ diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js index de63b200..0d84ccbf 100644 --- a/ext/js/pages/settings/dictionary-controller.js +++ b/ext/js/pages/settings/dictionary-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection, log} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {DictionaryWorker} from '../../language/dictionary-worker.js'; import {yomitan} from '../../yomitan.js'; @@ -41,21 +42,21 @@ class DictionaryEntry { /** @type {ChildNode[]} */ this._nodes = [...fragment.childNodes]; /** @type {HTMLInputElement} */ - this._enabledCheckbox = /** @type {HTMLInputElement} */ (fragment.querySelector('.dictionary-enabled')); + this._enabledCheckbox = querySelectorNotNull(fragment, '.dictionary-enabled'); /** @type {HTMLInputElement} */ - this._priorityInput = /** @type {HTMLInputElement} */ (fragment.querySelector('.dictionary-priority')); + this._priorityInput = querySelectorNotNull(fragment, '.dictionary-priority'); /** @type {HTMLButtonElement} */ - this._menuButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-menu-button')); + this._menuButton = querySelectorNotNull(fragment, '.dictionary-menu-button'); /** @type {HTMLButtonElement} */ - this._outdatedButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-outdated-button')); + this._outdatedButton = querySelectorNotNull(fragment, '.dictionary-outdated-button'); /** @type {HTMLButtonElement} */ - this._integrityButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-integrity-button')); + this._integrityButton = querySelectorNotNull(fragment, '.dictionary-integrity-button'); /** @type {HTMLElement} */ - this._titleNode = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-title')); + this._titleNode = querySelectorNotNull(fragment, '.dictionary-title'); /** @type {HTMLElement} */ - this._versionNode = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-version')); + this._versionNode = querySelectorNotNull(fragment, '.dictionary-version'); /** @type {HTMLElement} */ - this._titleContainer = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-item-title-container')); + this._titleContainer = querySelectorNotNull(fragment, '.dictionary-item-title-container'); } /** @type {string} */ @@ -168,12 +169,25 @@ class DictionaryEntry { const modal = this._dictionaryController.modalController.getModal('dictionary-details'); if (modal === null) { return; } - /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-title')).textContent = title; - /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-version')).textContent = `rev.${revision}`; - /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-outdated-notification')).hidden = (version >= 3); - /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-counts')).textContent = this._counts !== null ? JSON.stringify(this._counts, null, 4) : ''; - /** @type {HTMLInputElement} */ (modal.node.querySelector('.dictionary-prefix-wildcard-searches-supported')).checked = prefixWildcardsSupported; - this._setupDetails(/** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-details-table'))); + /** @type {HTMLElement} */ + const titleElement = querySelectorNotNull(modal.node, '.dictionary-title'); + /** @type {HTMLElement} */ + const versionElement = querySelectorNotNull(modal.node, '.dictionary-version'); + /** @type {HTMLElement} */ + const outdateElement = querySelectorNotNull(modal.node, '.dictionary-outdated-notification'); + /** @type {HTMLElement} */ + const countsElement = querySelectorNotNull(modal.node, '.dictionary-counts'); + /** @type {HTMLInputElement} */ + const wildcardSupportedElement = querySelectorNotNull(modal.node, '.dictionary-prefix-wildcard-searches-supported'); + /** @type {HTMLElement} */ + const detailsTableElement = querySelectorNotNull(modal.node, '.dictionary-details-table'); + + titleElement.textContent = title; + versionElement.textContent = `rev.${revision}`; + outdateElement.hidden = (version >= 3); + countsElement.textContent = this._counts !== null ? JSON.stringify(this._counts, null, 4) : ''; + wildcardSupportedElement.checked = prefixWildcardsSupported; + this._setupDetails(detailsTableElement); modal.setVisible(true); } @@ -200,8 +214,14 @@ class DictionaryEntry { const details = /** @type {HTMLElement} */ (this._dictionaryController.instantiateTemplate('dictionary-details-entry')); details.dataset.type = key; - /** @type {HTMLElement} */ (details.querySelector('.dictionary-details-entry-label')).textContent = `${label}:`; - /** @type {HTMLElement} */ (details.querySelector('.dictionary-details-entry-info')).textContent = info; + + /** @type {HTMLElement} */ + const labelElement = querySelectorNotNull(details, '.dictionary-details-entry-label'); + /** @type {HTMLElement} */ + const infoElement = querySelectorNotNull(details, '.dictionary-details-entry-info'); + + labelElement.textContent = `${label}:`; + infoElement.textContent = info; fragment.appendChild(details); any = true; @@ -241,8 +261,10 @@ class DictionaryEntry { const count = this._dictionaryController.dictionaryOptionCount; const modal = this._dictionaryController.modalController.getModal('dictionary-move-location'); if (modal === null) { return; } - const input = /** @type {HTMLInputElement} */ (modal.node.querySelector('#dictionary-move-location')); - const titleNode = /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-title')); + /** @type {HTMLInputElement} */ + const input = querySelectorNotNull(modal.node, '#dictionary-move-location'); + /** @type {HTMLElement} */ + const titleNode = querySelectorNotNull(modal.node, '.dictionary-title'); modal.node.dataset.index = `${this._index}`; titleNode.textContent = title; @@ -284,9 +306,11 @@ class DictionaryExtraInfo { this._nodes.push(node); } - const dictionaryIntegrityButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-integrity-button')); + /** @type {HTMLButtonElement} */ + const dictionaryIntegrityButton = querySelectorNotNull(fragment, '.dictionary-integrity-button'); - this._setTitle(fragment.querySelector('.dictionary-total-count')); + const titleNode = fragment.querySelector('.dictionary-total-count'); + this._setTitle(titleNode); this._eventListeners.addEventListener(dictionaryIntegrityButton, 'click', this._onIntegrityButtonClick.bind(this), false); container.appendChild(fragment); @@ -315,11 +339,13 @@ class DictionaryExtraInfo { const modal = this._parent.modalController.getModal('dictionary-extra-data'); if (modal === null) { return; } - const dictionaryCounts = /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-counts')); + /** @type {HTMLElement} */ + const dictionaryCounts = querySelectorNotNull(modal.node, '.dictionary-counts'); const info = {counts: this._totalCounts, remainders: this._remainders}; dictionaryCounts.textContent = JSON.stringify(info, null, 4); - this._setTitle(modal.node.querySelector('.dictionary-total-count')); + const titleNode = modal.node.querySelector('.dictionary-total-count'); + this._setTitle(titleNode); modal.setVisible(true); } @@ -354,22 +380,22 @@ export class DictionaryController { this._databaseStateToken = null; /** @type {boolean} */ this._checkingIntegrity = false; - /** @type {?HTMLButtonElement} */ - this._checkIntegrityButton = null; - /** @type {?HTMLElement} */ - this._dictionaryEntryContainer = null; - /** @type {?HTMLElement} */ - this._dictionaryInstallCountNode = null; - /** @type {?HTMLElement} */ - this._dictionaryEnabledCountNode = null; + /** @type {HTMLButtonElement} */ + this._checkIntegrityButton = querySelectorNotNull(document, '#dictionary-check-integrity'); + /** @type {HTMLElement} */ + this._dictionaryEntryContainer = querySelectorNotNull(document, '#dictionary-list'); + /** @type {HTMLElement} */ + this._dictionaryInstallCountNode = querySelectorNotNull(document, '#dictionary-install-count'); + /** @type {HTMLElement} */ + this._dictionaryEnabledCountNode = querySelectorNotNull(document, '#dictionary-enabled-count'); /** @type {?NodeListOf} */ this._noDictionariesInstalledWarnings = null; /** @type {?NodeListOf} */ this._noDictionariesEnabledWarnings = null; /** @type {?import('./modal.js').Modal} */ this._deleteDictionaryModal = null; - /** @type {?HTMLInputElement} */ - this._allCheckbox = null; + /** @type {HTMLInputElement} */ + this._allCheckbox = querySelectorNotNull(document, '#all-dictionaries-enabled'); /** @type {?DictionaryExtraInfo} */ this._extraInfo = null; /** @type {boolean} */ @@ -388,16 +414,13 @@ export class DictionaryController { /** */ async prepare() { - this._checkIntegrityButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-check-integrity')); - this._dictionaryEntryContainer = /** @type {HTMLElement} */ (document.querySelector('#dictionary-list')); - this._dictionaryInstallCountNode = /** @type {HTMLElement} */ (document.querySelector('#dictionary-install-count')); - this._dictionaryEnabledCountNode = /** @type {HTMLElement} */ (document.querySelector('#dictionary-enabled-count')); this._noDictionariesInstalledWarnings = /** @type {NodeListOf} */ (document.querySelectorAll('.no-dictionaries-installed-warning')); this._noDictionariesEnabledWarnings = /** @type {NodeListOf} */ (document.querySelectorAll('.no-dictionaries-enabled-warning')); this._deleteDictionaryModal = this._modalController.getModal('dictionary-confirm-delete'); - this._allCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#all-dictionaries-enabled')); - const dictionaryDeleteButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-confirm-delete-button')); - const dictionaryMoveButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-move-button')); + /** @type {HTMLButtonElement} */ + const dictionaryDeleteButton = querySelectorNotNull(document, '#dictionary-confirm-delete-button'); + /** @type {HTMLButtonElement} */ + const dictionaryMoveButton = querySelectorNotNull(document, '#dictionary-move-button'); yomitan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); @@ -420,7 +443,8 @@ export class DictionaryController { if (this._isDeleting) { return; } const modal = /** @type {import('./modal.js').Modal} */ (this._deleteDictionaryModal); modal.node.dataset.dictionaryTitle = dictionaryTitle; - const nameElement = /** @type {Element} */ (modal.node.querySelector('#dictionary-confirm-delete-name')); + /** @type {Element} */ + const nameElement = querySelectorNotNull(modal.node, '#dictionary-confirm-delete-name'); nameElement.textContent = dictionaryTitle; modal.setVisible(true); } @@ -696,7 +720,9 @@ export class DictionaryController { if (typeof index !== 'number') { return; } const indexNumber = Number.parseInt(index, 10); - const targetString = /** @type {HTMLInputElement} */ (document.querySelector('#dictionary-move-location')).value; + /** @type {HTMLInputElement} */ + const targetStringInput = querySelectorNotNull(document, '#dictionary-move-location'); + const targetString = targetStringInput.value; const target = Number.parseInt(targetString, 10) - 1; if (!Number.isFinite(target) || !Number.isFinite(indexNumber) || indexNumber === target) { return; } diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index d1255e11..35b7c461 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -18,6 +18,7 @@ import {log} from '../../core.js'; import {ExtensionError} from '../../core/extension-error.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {DictionaryWorker} from '../../language/dictionary-worker.js'; import {yomitan} from '../../yomitan.js'; import {DictionaryController} from './dictionary-controller.js'; @@ -37,22 +38,18 @@ export class DictionaryImportController { this._statusFooter = statusFooter; /** @type {boolean} */ this._modifying = false; - /** @type {?HTMLButtonElement} */ - this._purgeButton = null; - /** @type {?HTMLButtonElement} */ - this._purgeConfirmButton = null; - /** @type {?HTMLButtonElement} */ - this._importFileButton = null; - /** @type {?HTMLInputElement} */ - this._importFileInput = null; + /** @type {HTMLButtonElement} */ + this._purgeButton = querySelectorNotNull(document, '#dictionary-delete-all-button'); + /** @type {HTMLButtonElement} */ + this._purgeConfirmButton = querySelectorNotNull(document, '#dictionary-confirm-delete-all-button'); + /** @type {HTMLButtonElement} */ + this._importFileButton = querySelectorNotNull(document, '#dictionary-import-file-button'); + /** @type {HTMLInputElement} */ + this._importFileInput = querySelectorNotNull(document, '#dictionary-import-file-input'); /** @type {?import('./modal.js').Modal} */ this._purgeConfirmModal = null; - /** @type {?HTMLElement} */ - this._errorContainer = null; - /** @type {?HTMLElement} */ - this._spinner = null; - /** @type {?HTMLElement} */ - this._purgeNotification = null; + /** @type {HTMLElement} */ + this._errorContainer = querySelectorNotNull(document, '#dictionary-error'); /** @type {[originalMessage: string, newMessage: string][]} */ this._errorToStringOverrides = [ [ @@ -68,14 +65,7 @@ export class DictionaryImportController { /** */ async prepare() { - this._purgeButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-delete-all-button')); - this._purgeConfirmButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-confirm-delete-all-button')); - this._importFileButton = /** @type {HTMLButtonElement} */ (document.querySelector('#dictionary-import-file-button')); - this._importFileInput = /** @type {HTMLInputElement} */ (document.querySelector('#dictionary-import-file-input')); this._purgeConfirmModal = this._modalController.getModal('dictionary-confirm-delete-all'); - this._errorContainer = /** @type {HTMLElement} */ (document.querySelector('#dictionary-error')); - this._spinner = /** @type {HTMLElement} */ (document.querySelector('#dictionary-spinner')); - this._purgeNotification = /** @type {HTMLElement} */ (document.querySelector('#dictionary-delete-all-status')); this._purgeButton.addEventListener('click', this._onPurgeButtonClick.bind(this), false); this._purgeConfirmButton.addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false); @@ -123,14 +113,11 @@ export class DictionaryImportController { async _purgeDatabase() { if (this._modifying) { return; } - const purgeNotification = this._purgeNotification; const prevention = this._preventPageExit(); try { this._setModifying(true); this._hideErrors(); - this._setSpinnerVisible(true); - if (purgeNotification !== null) { purgeNotification.hidden = false; } await yomitan.api.purgeDatabase(); const errors = await this._clearDictionarySettings(); @@ -142,8 +129,6 @@ export class DictionaryImportController { this._showErrors([error instanceof Error ? error : new Error(`${error}`)]); } finally { prevention.end(); - if (purgeNotification !== null) { purgeNotification.hidden = true; } - this._setSpinnerVisible(false); this._setModifying(false); this._triggerStorageChanged(); } @@ -156,7 +141,6 @@ export class DictionaryImportController { if (this._modifying) { return; } const statusFooter = this._statusFooter; - const importInfo = /** @type {HTMLElement} */ (document.querySelector('#dictionary-import-info')); const progressSelector = '.dictionary-import-progress'; const progressContainers = /** @type {NodeListOf} */ (document.querySelectorAll(`#dictionaries-modal ${progressSelector}`)); const progressBars = /** @type {NodeListOf} */ (document.querySelectorAll(`${progressSelector} .progress-bar`)); @@ -168,7 +152,6 @@ export class DictionaryImportController { try { this._setModifying(true); this._hideErrors(); - this._setSpinnerVisible(true); for (const progress of progressContainers) { progress.hidden = false; } @@ -204,11 +187,6 @@ export class DictionaryImportController { const fileCount = files.length; for (let i = 0; i < fileCount; ++i) { - if (importInfo !== null && fileCount > 1) { - importInfo.hidden = false; - importInfo.textContent = `(${i + 1} of ${fileCount})`; - } - statusPrefix = `Importing dictionary${fileCount > 1 ? ` (${i + 1} of ${fileCount})` : ''}`; onProgress({ stepIndex: -1, @@ -226,11 +204,6 @@ export class DictionaryImportController { prevention.end(); for (const progress of progressContainers) { progress.hidden = true; } if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); } - if (importInfo !== null) { - importInfo.textContent = ''; - importInfo.hidden = true; - } - this._setSpinnerVisible(false); this._setModifying(false); this._triggerStorageChanged(); } @@ -312,15 +285,6 @@ export class DictionaryImportController { return await this._modifyGlobalSettings(targets); } - /** - * @param {boolean} visible - */ - _setSpinnerVisible(visible) { - if (this._spinner !== null) { - this._spinner.hidden = !visible; - } - } - /** * @returns {import('settings-controller').PageExitPrevention} */ diff --git a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js index d36d965a..e92d9e93 100644 --- a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js +++ b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection, isObject} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {HotkeyUtil} from '../../input/hotkey-util.js'; import {yomitan} from '../../yomitan.js'; import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js'; @@ -28,12 +29,12 @@ export class ExtensionKeyboardShortcutController { constructor(settingsController) { /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; - /** @type {?HTMLButtonElement} */ - this._resetButton = null; - /** @type {?HTMLButtonElement} */ - this._clearButton = null; - /** @type {?HTMLElement} */ - this._listContainer = null; + /** @type {HTMLButtonElement} */ + this._resetButton = querySelectorNotNull(document, '#extension-hotkey-list-reset-all'); + /** @type {HTMLButtonElement} */ + this._clearButton = querySelectorNotNull(document, '#extension-hotkey-list-clear-all'); + /** @type {HTMLElement} */ + this._listContainer = querySelectorNotNull(document, '#extension-hotkey-list'); /** @type {HotkeyUtil} */ this._hotkeyUtil = new HotkeyUtil(); /** @type {?import('environment').OperatingSystem} */ @@ -49,10 +50,6 @@ export class ExtensionKeyboardShortcutController { /** */ async prepare() { - this._resetButton = /** @type {HTMLButtonElement} */ (document.querySelector('#extension-hotkey-list-reset-all')); - this._clearButton = /** @type {HTMLButtonElement} */ (document.querySelector('#extension-hotkey-list-clear-all')); - this._listContainer = /** @type {HTMLElement} */ (document.querySelector('#extension-hotkey-list')); - const canResetCommands = this.canResetCommands(); const canModifyCommands = this.canModifyCommands(); this._resetButton.hidden = !canResetCommands; @@ -277,11 +274,14 @@ class ExtensionKeyboardShortcutHotkeyEntry { /** */ prepare() { - const label = /** @type {HTMLElement} */ (this._node.querySelector('.settings-item-label')); + /** @type {HTMLElement} */ + const label = querySelectorNotNull(this._node, '.settings-item-label'); label.textContent = this._description || this._name; - const button = /** @type {HTMLButtonElement} */ (this._node.querySelector('.extension-hotkey-list-item-button')); - const input = /** @type {HTMLInputElement} */ (this._node.querySelector('input')); + /** @type {HTMLButtonElement} */ + const button = querySelectorNotNull(this._node, '.extension-hotkey-list-item-button'); + /** @type {HTMLInputElement} */ + const input = querySelectorNotNull(this._node, 'input'); this._input = input; diff --git a/ext/js/pages/settings/keyboard-shortcuts-controller.js b/ext/js/pages/settings/keyboard-shortcuts-controller.js index ad16b0e9..0734faa1 100644 --- a/ext/js/pages/settings/keyboard-shortcuts-controller.js +++ b/ext/js/pages/settings/keyboard-shortcuts-controller.js @@ -18,6 +18,7 @@ import {EventListenerCollection} from '../../core.js'; import {DocumentUtil} from '../../dom/document-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js'; import {yomitan} from '../../yomitan.js'; import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js'; @@ -33,18 +34,18 @@ export class KeyboardShortcutController { 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 {HTMLButtonElement} */ + this._addButton = querySelectorNotNull(document, '#hotkey-list-add'); + /** @type {HTMLButtonElement} */ + this._resetButton = querySelectorNotNull(document, '#hotkey-list-reset'); + /** @type {HTMLElement} */ + this._listContainer = querySelectorNotNull(document, '#hotkey-list'); + /** @type {HTMLElement} */ + this._emptyIndicator = querySelectorNotNull(document, '#hotkey-list-empty'); /** @type {Intl.Collator} */ this._stringComparer = new Intl.Collator('en-US'); // Invariant locale - /** @type {?HTMLElement} */ - this._scrollContainer = null; + /** @type {HTMLElement} */ + this._scrollContainer = querySelectorNotNull(document, '#keyboard-shortcuts-modal .modal-body'); /** @type {Map} */ this._actionDetails = new Map([ ['', {scopes: new Set()}], @@ -81,12 +82,6 @@ export class KeyboardShortcutController { const {platform: {os}} = await yomitan.api.getEnvironmentInfo(); this._os = os; - 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)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); @@ -283,12 +278,18 @@ class KeyboardShortcutHotkeyEntry { prepare() { const node = this._node; - 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')); + /** @type {HTMLButtonElement} */ + const menuButton = querySelectorNotNull(node, '.hotkey-list-item-button'); + /** @type {HTMLInputElement} */ + const input = querySelectorNotNull(node, '.hotkey-list-item-input'); + /** @type {HTMLSelectElement} */ + const action = querySelectorNotNull(node, '.hotkey-list-item-action'); + /** @type {HTMLInputElement} */ + const enabledToggle = querySelectorNotNull(node, '.hotkey-list-item-enabled'); + /** @type {HTMLButtonElement} */ + const scopesButton = querySelectorNotNull(node, '.hotkey-list-item-scopes-button'); + /** @type {HTMLButtonElement} */ + const enabledButton = querySelectorNotNull(node, '.hotkey-list-item-enabled-button'); this._actionSelect = action; this._enabledButton = enabledButton; @@ -333,7 +334,8 @@ class KeyboardShortcutHotkeyEntry { const {action} = this._data; const {menu} = e.detail; - const resetArgument = /** @type {HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action="resetArgument"]')); + /** @type {HTMLElement} */ + const resetArgument = querySelectorNotNull(menu.bodyNode, '.popup-menu-item[data-menu-action="resetArgument"]'); const details = this._parent.getActionDetails(action); const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0; @@ -646,7 +648,8 @@ class KeyboardShortcutHotkeyEntry { if (scope === null) { continue; } menuItem.hidden = !(validScopes === null || validScopes.has(scope)); - const checkbox = /** @type {HTMLInputElement} */ (menuItem.querySelector('.hotkey-scope-checkbox')); + /** @type {HTMLInputElement} */ + const checkbox = querySelectorNotNull(menuItem, '.hotkey-scope-checkbox'); if (checkbox !== null) { checkbox.checked = scopes.includes(scope); this._scopeMenuEventListeners.addEventListener(checkbox, 'change', this._onScopeCheckboxChange.bind(this), false); diff --git a/ext/js/pages/settings/mecab-controller.js b/ext/js/pages/settings/mecab-controller.js index 4e2b02c6..9c55c9a0 100644 --- a/ext/js/pages/settings/mecab-controller.js +++ b/ext/js/pages/settings/mecab-controller.js @@ -16,23 +16,21 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class MecabController { constructor() { - /** @type {?HTMLButtonElement} */ - this._testButton = null; - /** @type {?HTMLElement} */ - this._resultsContainer = null; + /** @type {HTMLButtonElement} */ + this._testButton = querySelectorNotNull(document, '#test-mecab-button'); + /** @type {HTMLElement} */ + this._resultsContainer = querySelectorNotNull(document, '#test-mecab-results'); /** @type {boolean} */ this._testActive = false; } /** */ prepare() { - this._testButton = /** @type {HTMLButtonElement} */ (document.querySelector('#test-mecab-button')); - this._resultsContainer = /** @type {HTMLElement} */ (document.querySelector('#test-mecab-results')); - this._testButton.addEventListener('click', this._onTestButtonClick.bind(this), false); } diff --git a/ext/js/pages/settings/modal.js b/ext/js/pages/settings/modal.js index 21a6e705..17a4605d 100644 --- a/ext/js/pages/settings/modal.js +++ b/ext/js/pages/settings/modal.js @@ -37,7 +37,8 @@ export class Modal extends PanelElement { prepare() { const node = this.node; this._contentNode = node.querySelector('.modal-content'); - let dimmerNode = /** @type {?HTMLElement} */ (node.querySelector('.modal-content-dimmer')); + /** @type {?HTMLElement} */ + let dimmerNode = node.querySelector('.modal-content-dimmer'); if (dimmerNode === null) { dimmerNode = node; } dimmerNode.addEventListener('mousedown', this._onModalContainerMouseDown.bind(this), false); dimmerNode.addEventListener('mouseup', this._onModalContainerMouseUp.bind(this), false); diff --git a/ext/js/pages/settings/nested-popups-controller.js b/ext/js/pages/settings/nested-popups-controller.js index c01986ab..7eb78148 100644 --- a/ext/js/pages/settings/nested-popups-controller.js +++ b/ext/js/pages/settings/nested-popups-controller.js @@ -17,6 +17,7 @@ */ import {DocumentUtil} from '../../dom/document-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; export class NestedPopupsController { /** @@ -27,20 +28,16 @@ export class NestedPopupsController { this._settingsController = settingsController; /** @type {number} */ this._popupNestingMaxDepth = 0; - /** @type {?HTMLInputElement} */ - this._nestedPopupsEnabled = null; - /** @type {?HTMLInputElement} */ - this._nestedPopupsCount = null; - /** @type {?HTMLElement} */ - this._nestedPopupsEnabledMoreOptions = null; + /** @type {HTMLInputElement} */ + this._nestedPopupsEnabled = querySelectorNotNull(document, '#nested-popups-enabled'); + /** @type {HTMLInputElement} */ + this._nestedPopupsCount = querySelectorNotNull(document, '#nested-popups-count'); + /** @type {HTMLElement} */ + this._nestedPopupsEnabledMoreOptions = querySelectorNotNull(document, '#nested-popups-enabled-more-options'); } /** */ async prepare() { - this._nestedPopupsEnabled = /** @type {HTMLInputElement} */ (document.querySelector('#nested-popups-enabled')); - this._nestedPopupsCount = /** @type {HTMLInputElement} */ (document.querySelector('#nested-popups-count')); - this._nestedPopupsEnabledMoreOptions = /** @type {HTMLElement} */ (document.querySelector('#nested-popups-enabled-more-options')); - const options = await this._settingsController.getOptions(); const optionsContext = this._settingsController.getOptionsContext(); diff --git a/ext/js/pages/settings/permissions-origin-controller.js b/ext/js/pages/settings/permissions-origin-controller.js index a4271f92..6dacced8 100644 --- a/ext/js/pages/settings/permissions-origin-controller.js +++ b/ext/js/pages/settings/permissions-origin-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; export class PermissionsOriginController { /** @@ -25,16 +26,16 @@ export class PermissionsOriginController { constructor(settingsController) { /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; - /** @type {?HTMLElement} */ - this._originContainer = null; - /** @type {?HTMLElement} */ - this._originEmpty = null; + /** @type {HTMLElement} */ + this._originContainer = querySelectorNotNull(document, '#permissions-origin-list'); + /** @type {HTMLElement} */ + this._originEmpty = querySelectorNotNull(document, '#permissions-origin-list-empty'); /** @type {?NodeListOf} */ this._originToggleNodes = null; - /** @type {?HTMLInputElement} */ - this._addOriginInput = null; - /** @type {?HTMLElement} */ - this._errorContainer = null; + /** @type {HTMLInputElement} */ + this._addOriginInput = querySelectorNotNull(document, '#permissions-origin-new-input'); + /** @type {HTMLElement} */ + this._errorContainer = querySelectorNotNull(document, '#permissions-origin-list-error'); /** @type {ChildNode[]} */ this._originContainerChildren = []; /** @type {EventListenerCollection} */ @@ -43,12 +44,9 @@ export class PermissionsOriginController { /** */ async prepare() { - this._originContainer = /** @type {HTMLElement} */ (document.querySelector('#permissions-origin-list')); - this._originEmpty = /** @type {HTMLElement} */ (document.querySelector('#permissions-origin-list-empty')); this._originToggleNodes = /** @type {NodeListOf} */ (document.querySelectorAll('.permissions-origin-toggle')); - this._addOriginInput = /** @type {HTMLInputElement} */ (document.querySelector('#permissions-origin-new-input')); - this._errorContainer = /** @type {HTMLElement} */ (document.querySelector('#permissions-origin-list-error')); - const addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#permissions-origin-add')); + /** @type {HTMLButtonElement} */ + const addButton = querySelectorNotNull(document, '#permissions-origin-add'); for (const node of this._originToggleNodes) { node.addEventListener('change', this._onOriginToggleChange.bind(this), false); @@ -87,8 +85,10 @@ export class PermissionsOriginController { for (const origin of originsSet) { if (excludeOrigins.has(origin)) { continue; } const node = this._settingsController.instantiateTemplateFragment('permissions-origin'); - const input = /** @type {HTMLInputElement} */ (node.querySelector('.permissions-origin-input')); - const menuButton = /** @type {HTMLElement} */ (node.querySelector('.permissions-origin-button')); + /** @type {HTMLInputElement} */ + const input = querySelectorNotNull(node, '.permissions-origin-input'); + /** @type {HTMLElement} */ + const menuButton = querySelectorNotNull(node, '.permissions-origin-button'); input.value = origin; this._eventListeners.addEventListener(menuButton, 'menuClose', this._onOriginMenuClose.bind(this, origin), false); this._originContainerChildren.push(...node.childNodes); diff --git a/ext/js/pages/settings/persistent-storage-controller.js b/ext/js/pages/settings/persistent-storage-controller.js index e85bfc6b..7386edd7 100644 --- a/ext/js/pages/settings/persistent-storage-controller.js +++ b/ext/js/pages/settings/persistent-storage-controller.js @@ -17,22 +17,23 @@ */ import {isObject} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class PersistentStorageController { constructor() { - /** @type {?HTMLInputElement} */ - this._persistentStorageCheckbox = null; + /** @type {HTMLInputElement} */ + this._persistentStorageCheckbox = querySelectorNotNull(document, '#storage-persistent-checkbox'); } /** */ async prepare() { - this._persistentStorageCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#storage-persistent-checkbox')); this._persistentStorageCheckbox.addEventListener('change', this._onPersistentStorageCheckboxChange.bind(this), false); if (!this._isPersistentStorageSupported()) { return; } - const info = /** @type {?HTMLElement} */ (document.querySelector('#storage-persistent-info')); + /** @type {?HTMLElement} */ + const info = document.querySelector('#storage-persistent-info'); if (info !== null) { info.hidden = false; } const isStoragePeristent = await this.isStoragePeristent(); @@ -77,7 +78,8 @@ export class PersistentStorageController { this._updateCheckbox(isStoragePeristent); - const node = /** @type {?HTMLElement} */ (document.querySelector('#storage-persistent-fail-warning')); + /** @type {?HTMLElement} */ + const node = document.querySelector('#storage-persistent-fail-warning'); if (node !== null) { node.hidden = isStoragePeristent; } yomitan.trigger('storageChanged'); diff --git a/ext/js/pages/settings/popup-preview-controller.js b/ext/js/pages/settings/popup-preview-controller.js index 7239ca17..d8bc9850 100644 --- a/ext/js/pages/settings/popup-preview-controller.js +++ b/ext/js/pages/settings/popup-preview-controller.js @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; + export class PopupPreviewController { /** * @param {import('./settings-controller.js').SettingsController} settingsController @@ -25,25 +27,20 @@ export class PopupPreviewController { this._settingsController = settingsController; /** @type {string} */ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - /** @type {?HTMLIFrameElement} */ - this._frame = null; - /** @type {?HTMLTextAreaElement} */ - this._customCss = null; - /** @type {?HTMLTextAreaElement} */ - this._customOuterCss = null; - /** @type {?HTMLElement} */ - this._previewFrameContainer = null; + /** @type {HTMLIFrameElement} */ + this._frame = querySelectorNotNull(document, '#popup-preview-frame'); + /** @type {HTMLTextAreaElement} */ + this._customCss = querySelectorNotNull(document, '#custom-popup-css'); + /** @type {HTMLTextAreaElement} */ + this._customOuterCss = querySelectorNotNull(document, '#custom-popup-outer-css'); + /** @type {HTMLElement} */ + this._previewFrameContainer = querySelectorNotNull(document, '.preview-frame-container'); } /** */ async prepare() { if (new URLSearchParams(location.search).get('popup-preview') === 'false') { return; } - this._frame = /** @type {HTMLIFrameElement} */ (document.querySelector('#popup-preview-frame')); - this._customCss = /** @type {HTMLTextAreaElement} */ (document.querySelector('#custom-popup-css')); - this._customOuterCss = /** @type {HTMLTextAreaElement} */ (document.querySelector('#custom-popup-outer-css')); - this._previewFrameContainer = /** @type {HTMLElement} */ (document.querySelector('.preview-frame-container')); - this._customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); this._customCss.addEventListener('settingChanged', this._onCustomCssChange.bind(this), false); this._customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js index 60d264fa..e5cc6580 100644 --- a/ext/js/pages/settings/popup-preview-frame.js +++ b/ext/js/pages/settings/popup-preview-frame.js @@ -18,6 +18,7 @@ import * as wanakana from '../../../lib/wanakana.js'; import {Frontend} from '../../app/frontend.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {TextSourceRange} from '../../dom/text-source-range.js'; import {yomitan} from '../../yomitan.js'; @@ -49,10 +50,10 @@ export class PopupPreviewFrame { this._textSource = null; /** @type {?import('settings').OptionsContext} */ this._optionsContext = null; - /** @type {?HTMLElement} */ - this._exampleText = null; - /** @type {?HTMLInputElement} */ - this._exampleTextInput = null; + /** @type {HTMLElement} */ + this._exampleText = querySelectorNotNull(document, '#example-text'); + /** @type {HTMLInputElement} */ + this._exampleTextInput = querySelectorNotNull(document, '#example-text-input'); /** @type {string} */ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); @@ -67,9 +68,6 @@ export class PopupPreviewFrame { /** */ async prepare() { - this._exampleText = /** @type {HTMLElement} */ (document.querySelector('#example-text')); - this._exampleTextInput = /** @type {HTMLInputElement} */ (document.querySelector('#example-text-input')); - if (this._exampleTextInput !== null && typeof wanakana !== 'undefined') { wanakana.bind(this._exampleTextInput); } @@ -77,7 +75,8 @@ export class PopupPreviewFrame { window.addEventListener('message', this._onMessage.bind(this), false); // Setup events - const darkThemeCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#theme-dark-checkbox')); + /** @type {HTMLInputElement} */ + const darkThemeCheckbox = querySelectorNotNull(document, '#theme-dark-checkbox'); darkThemeCheckbox.addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false); this._exampleText.addEventListener('click', this._onExampleTextClick.bind(this), false); this._exampleTextInput.addEventListener('blur', this._onExampleTextInputBlur.bind(this), false); diff --git a/ext/js/pages/settings/popup-window-controller.js b/ext/js/pages/settings/popup-window-controller.js index e1a5456b..0d56dc58 100644 --- a/ext/js/pages/settings/popup-window-controller.js +++ b/ext/js/pages/settings/popup-window-controller.js @@ -16,12 +16,14 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class PopupWindowController { /** */ prepare() { - const testLink = /** @type {HTMLElement} */ (document.querySelector('#test-window-open-link')); + /** @type {HTMLElement} */ + const testLink = querySelectorNotNull(document, '#test-window-open-link'); testLink.addEventListener('click', this._onTestWindowOpenLinkClick.bind(this), false); } diff --git a/ext/js/pages/settings/profile-conditions-ui.js b/ext/js/pages/settings/profile-conditions-ui.js index 96aef83f..e02d2585 100644 --- a/ext/js/pages/settings/profile-conditions-ui.js +++ b/ext/js/pages/settings/profile-conditions-ui.js @@ -18,6 +18,7 @@ import {EventDispatcher, EventListenerCollection} from '../../core.js'; import {DocumentUtil} from '../../dom/document-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js'; /** @@ -33,10 +34,10 @@ export class ProfileConditionsUI extends EventDispatcher { this._settingsController = settingsController; /** @type {?import('environment').OperatingSystem} */ this._os = null; - /** @type {?HTMLElement} */ - this._conditionGroupsContainer = null; - /** @type {?HTMLElement} */ - this._addConditionGroupButton = null; + /** @type {HTMLElement} */ + this._conditionGroupsContainer = querySelectorNotNull(document, '#profile-condition-groups'); + /** @type {HTMLElement} */ + this._addConditionGroupButton = querySelectorNotNull(document, '#profile-add-condition-group'); /** @type {ProfileConditionGroupUI[]} */ this._children = []; /** @type {EventListenerCollection} */ @@ -139,8 +140,6 @@ export class ProfileConditionsUI extends EventDispatcher { const {conditionGroups} = profiles[profileIndex]; this._profileIndex = profileIndex; - this._conditionGroupsContainer = /** @type {HTMLElement} */ (document.querySelector('#profile-condition-groups')); - this._addConditionGroupButton = /** @type {HTMLElement} */ (document.querySelector('#profile-add-condition-group')); for (let i = 0, ii = conditionGroups.length; i < ii; ++i) { this._addConditionGroup(conditionGroups[i], i); @@ -157,9 +156,6 @@ export class ProfileConditionsUI extends EventDispatcher { child.cleanup(); } this._children = []; - - this._conditionGroupsContainer = null; - this._addConditionGroupButton = null; } /** @@ -354,7 +350,7 @@ export class ProfileConditionsUI extends EventDispatcher { const child = new ProfileConditionGroupUI(this, index); child.prepare(conditionGroup); this._children.push(child); - /** @type {HTMLElement} */ (this._conditionGroupsContainer).appendChild(child.node); + this._conditionGroupsContainer.appendChild(child.node); return child; } @@ -460,9 +456,9 @@ class ProfileConditionGroupUI { /** @type {HTMLElement} */ this._node = /** @type {HTMLElement} */ (this._parent.instantiateTemplate('profile-condition-group')); /** @type {HTMLElement} */ - this._conditionContainer = /** @type {HTMLElement} */ (this._node.querySelector('.profile-condition-list')); + this._conditionContainer = querySelectorNotNull(this._node, '.profile-condition-list'); /** @type {HTMLElement} */ - this._addConditionButton = /** @type {HTMLElement} */ (this._node.querySelector('.profile-condition-add-button')); + this._addConditionButton = querySelectorNotNull(this._node, '.profile-condition-add-button'); /** @type {ProfileConditionUI[]} */ this._children = []; /** @type {EventListenerCollection} */ @@ -621,23 +617,23 @@ class ProfileConditionUI { /** @type {HTMLElement} */ this._node = this._parent.parent.instantiateTemplate('profile-condition'); /** @type {HTMLSelectElement} */ - this._typeInput = /** @type {HTMLSelectElement} */ (this._node.querySelector('.profile-condition-type')); + this._typeInput = querySelectorNotNull(this._node, '.profile-condition-type'); /** @type {HTMLSelectElement} */ - this._operatorInput = /** @type {HTMLSelectElement} */ (this._node.querySelector('.profile-condition-operator')); + this._operatorInput = querySelectorNotNull(this._node, '.profile-condition-operator'); /** @type {HTMLButtonElement} */ - this._removeButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.profile-condition-remove')); + this._removeButton = querySelectorNotNull(this._node, '.profile-condition-remove'); /** @type {HTMLButtonElement} */ - this._mouseButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.mouse-button')); + this._mouseButton = querySelectorNotNull(this._node, '.mouse-button'); /** @type {HTMLElement} */ - this._mouseButtonContainer = /** @type {HTMLElement} */ (this._node.querySelector('.mouse-button-container')); + this._mouseButtonContainer = querySelectorNotNull(this._node, '.mouse-button-container'); /** @type {HTMLButtonElement} */ - this._menuButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.profile-condition-menu-button')); + this._menuButton = querySelectorNotNull(this._node, '.profile-condition-menu-button'); /** @type {HTMLElement} */ - this._typeOptionContainer = /** @type {HTMLElement} */ (this._typeInput.querySelector('optgroup')); + this._typeOptionContainer = querySelectorNotNull(this._typeInput, 'optgroup'); /** @type {HTMLElement} */ - this._operatorOptionContainer = /** @type {HTMLElement} */ (this._operatorInput.querySelector('optgroup')); + this._operatorOptionContainer = querySelectorNotNull(this._operatorInput, 'optgroup'); /** @type {HTMLInputElement} */ - this._valueInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.profile-condition-input')); + this._valueInput = querySelectorNotNull(this._node, '.profile-condition-input'); /** @type {string} */ this._value = ''; /** @type {?KeyboardMouseInputField} */ @@ -777,7 +773,8 @@ class ProfileConditionUI { */ _onMenuOpen(e) { const bodyNode = e.detail.menu.bodyNode; - const deleteGroup = /** @type {HTMLElement} */ (bodyNode.querySelector('.popup-menu-item[data-menu-action="deleteGroup"]')); + /** @type {HTMLElement} */ + const deleteGroup = querySelectorNotNull(bodyNode, '.popup-menu-item[data-menu-action="deleteGroup"]'); if (deleteGroup !== null) { deleteGroup.hidden = (this._parent.childCount <= 1); } diff --git a/ext/js/pages/settings/profile-controller.js b/ext/js/pages/settings/profile-controller.js index c82223b8..6e0710a8 100644 --- a/ext/js/pages/settings/profile-controller.js +++ b/ext/js/pages/settings/profile-controller.js @@ -17,6 +17,7 @@ */ import {clone, EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; import {ProfileConditionsUI} from './profile-conditions-ui.js'; @@ -34,24 +35,24 @@ export class ProfileController { this._profileConditionsUI = new ProfileConditionsUI(settingsController); /** @type {?number} */ this._profileConditionsIndex = null; - /** @type {?HTMLSelectElement} */ - this._profileActiveSelect = null; - /** @type {?HTMLSelectElement} */ - this._profileTargetSelect = null; - /** @type {?HTMLSelectElement} */ - this._profileCopySourceSelect = null; - /** @type {?HTMLElement} */ - this._removeProfileNameElement = null; - /** @type {?HTMLButtonElement} */ - this._profileAddButton = null; - /** @type {?HTMLButtonElement} */ - this._profileRemoveConfirmButton = null; - /** @type {?HTMLButtonElement} */ - this._profileCopyConfirmButton = null; - /** @type {?HTMLElement} */ - this._profileEntryListContainer = null; - /** @type {?HTMLElement} */ - this._profileConditionsProfileName = null; + /** @type {HTMLSelectElement} */ + this._profileActiveSelect = querySelectorNotNull(document, '#profile-active-select'); + /** @type {HTMLSelectElement} */ + this._profileTargetSelect = querySelectorNotNull(document, '#profile-target-select'); + /** @type {HTMLSelectElement} */ + this._profileCopySourceSelect = querySelectorNotNull(document, '#profile-copy-source-select'); + /** @type {HTMLElement} */ + this._removeProfileNameElement = querySelectorNotNull(document, '#profile-remove-name'); + /** @type {HTMLButtonElement} */ + this._profileAddButton = querySelectorNotNull(document, '#profile-add-button'); + /** @type {HTMLButtonElement} */ + this._profileRemoveConfirmButton = querySelectorNotNull(document, '#profile-remove-confirm-button'); + /** @type {HTMLButtonElement} */ + this._profileCopyConfirmButton = querySelectorNotNull(document, '#profile-copy-confirm-button'); + /** @type {HTMLElement} */ + this._profileEntryListContainer = querySelectorNotNull(document, '#profile-entry-list'); + /** @type {HTMLElement} */ + this._profileConditionsProfileName = querySelectorNotNull(document, '#profile-conditions-profile-name'); /** @type {?import('./modal.js').Modal} */ this._profileRemoveModal = null; /** @type {?import('./modal.js').Modal} */ @@ -83,15 +84,6 @@ export class ProfileController { const {platform: {os}} = await yomitan.api.getEnvironmentInfo(); this._profileConditionsUI.os = os; - this._profileActiveSelect = /** @type {HTMLSelectElement} */ (document.querySelector('#profile-active-select')); - this._profileTargetSelect = /** @type {HTMLSelectElement} */ (document.querySelector('#profile-target-select')); - this._profileCopySourceSelect = /** @type {HTMLSelectElement} */ (document.querySelector('#profile-copy-source-select')); - this._removeProfileNameElement = /** @type {HTMLElement} */ (document.querySelector('#profile-remove-name')); - this._profileAddButton = /** @type {HTMLButtonElement} */ (document.querySelector('#profile-add-button')); - this._profileRemoveConfirmButton = /** @type {HTMLButtonElement} */ (document.querySelector('#profile-remove-confirm-button')); - this._profileCopyConfirmButton = /** @type {HTMLButtonElement} */ (document.querySelector('#profile-copy-confirm-button')); - this._profileEntryListContainer = /** @type {HTMLElement} */ (document.querySelector('#profile-entry-list')); - this._profileConditionsProfileName = /** @type {HTMLElement} */ (document.querySelector('#profile-conditions-profile-name')); this._profileRemoveModal = this._modalController.getModal('profile-remove'); this._profileCopyModal = this._modalController.getModal('profile-copy'); this._profileConditionsModal = this._modalController.getModal('profile-conditions'); @@ -662,15 +654,15 @@ class ProfileEntry { /** @type {number} */ this._index = index; /** @type {HTMLInputElement} */ - this._isDefaultRadio = /** @type {HTMLInputElement} */ (node.querySelector('.profile-entry-is-default-radio')); + this._isDefaultRadio = querySelectorNotNull(node, '.profile-entry-is-default-radio'); /** @type {HTMLInputElement} */ - this._nameInput = /** @type {HTMLInputElement} */ (node.querySelector('.profile-entry-name-input')); + this._nameInput = querySelectorNotNull(node, '.profile-entry-name-input'); /** @type {HTMLElement} */ - this._countLink = /** @type {HTMLElement} */ (node.querySelector('.profile-entry-condition-count-link')); + this._countLink = querySelectorNotNull(node, '.profile-entry-condition-count-link'); /** @type {HTMLElement} */ - this._countText = /** @type {HTMLElement} */ (node.querySelector('.profile-entry-condition-count')); + this._countText = querySelectorNotNull(node, '.profile-entry-condition-count'); /** @type {HTMLButtonElement} */ - this._menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.profile-entry-menu-button')); + this._menuButton = querySelectorNotNull(node, '.profile-entry-menu-button'); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); } diff --git a/ext/js/pages/settings/scan-inputs-controller.js b/ext/js/pages/settings/scan-inputs-controller.js index 53423bdc..eb526863 100644 --- a/ext/js/pages/settings/scan-inputs-controller.js +++ b/ext/js/pages/settings/scan-inputs-controller.js @@ -18,6 +18,7 @@ import {EventListenerCollection} from '../../core.js'; import {DocumentUtil} from '../../dom/document-util.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js'; @@ -30,10 +31,10 @@ export class ScanInputsController { this._settingsController = settingsController; /** @type {?import('environment').OperatingSystem} */ this._os = null; - /** @type {?HTMLElement} */ - this._container = null; - /** @type {?HTMLButtonElement} */ - this._addButton = null; + /** @type {HTMLElement} */ + this._container = querySelectorNotNull(document, '#scan-input-list'); + /** @type {HTMLButtonElement} */ + this._addButton = querySelectorNotNull(document, '#scan-input-add'); /** @type {?NodeListOf} */ this._scanningInputCountNodes = null; /** @type {ScanInputField[]} */ @@ -45,8 +46,6 @@ export class ScanInputsController { const {platform: {os}} = await yomitan.api.getEnvironmentInfo(); this._os = os; - this._container = /** @type {HTMLElement} */ (document.querySelector('#scan-input-list')); - this._addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#scan-input-add')); this._scanningInputCountNodes = /** @type {NodeListOf} */ (document.querySelectorAll('.scanning-input-count')); this._addButton.addEventListener('click', this._onAddButtonClick.bind(this), false); @@ -157,7 +156,8 @@ export class ScanInputsController { // Scroll to bottom const button = /** @type {HTMLElement} */ (e.currentTarget); const modalContainer = /** @type {HTMLElement} */ (button.closest('.modal')); - const scrollContainer = /** @type {HTMLElement} */ (modalContainer.querySelector('.modal-body')); + /** @type {HTMLElement} */ + const scrollContainer = querySelectorNotNull(modalContainer, '.modal-body'); scrollContainer.scrollTop = scrollContainer.scrollHeight; } @@ -265,12 +265,16 @@ class ScanInputField { const {include, exclude, options: {showAdvanced}} = scanningInput; const node = /** @type {HTMLElement} */ (this._parent.instantiateTemplate('scan-input')); - const includeInputNode = /** @type {HTMLInputElement} */ (node.querySelector('.scan-input-field[data-property=include]')); - const includeMouseButton = /** @type {HTMLButtonElement} */ (node.querySelector('.mouse-button[data-property=include]')); - const excludeInputNode = /** @type {HTMLInputElement} */ (node.querySelector('.scan-input-field[data-property=exclude]')); - const excludeMouseButton = /** @type {HTMLButtonElement} */ (node.querySelector('.mouse-button[data-property=exclude]')); - const removeButton = /** @type {HTMLButtonElement} */ (node.querySelector('.scan-input-remove')); - const menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.scanning-input-menu-button')); + /** @type {HTMLInputElement} */ + const includeInputNode = querySelectorNotNull(node, '.scan-input-field[data-property=include]'); + /** @type {HTMLButtonElement} */ + const includeMouseButton = querySelectorNotNull(node, '.mouse-button[data-property=include]'); + /** @type {HTMLInputElement} */ + const excludeInputNode = querySelectorNotNull(node, '.scan-input-field[data-property=exclude]'); + /** @type {HTMLButtonElement} */ + const excludeMouseButton = querySelectorNotNull(node, '.mouse-button[data-property=exclude]'); + /** @type {HTMLButtonElement} */ + const menuButton = querySelectorNotNull(node, '.scanning-input-menu-button'); node.dataset.showAdvanced = `${showAdvanced}`; @@ -285,13 +289,8 @@ class ScanInputField { this._eventListeners.on(this._includeInputField, 'change', this._onIncludeValueChange.bind(this)); this._eventListeners.on(this._excludeInputField, 'change', this._onExcludeValueChange.bind(this)); - if (removeButton !== null) { - this._eventListeners.addEventListener(removeButton, 'click', this._onRemoveClick.bind(this)); - } - if (menuButton !== null) { - this._eventListeners.addEventListener(menuButton, 'menuOpen', this._onMenuOpen.bind(this)); - this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this)); - } + this._eventListeners.addEventListener(menuButton, 'menuOpen', this._onMenuOpen.bind(this)); + this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this)); this._updateDataSettingTargets(); } @@ -341,8 +340,10 @@ class ScanInputField { */ _onMenuOpen(e) { const bodyNode = e.detail.menu.bodyNode; - const showAdvanced = /** @type {?HTMLElement} */ (bodyNode.querySelector('.popup-menu-item[data-menu-action="showAdvanced"]')); - const hideAdvanced = /** @type {?HTMLElement} */ (bodyNode.querySelector('.popup-menu-item[data-menu-action="hideAdvanced"]')); + /** @type {?HTMLElement} */ + const showAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="showAdvanced"]'); + /** @type {?HTMLElement} */ + const hideAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="hideAdvanced"]'); const advancedVisible = (this._node !== null && this._node.dataset.showAdvanced === 'true'); if (showAdvanced !== null) { showAdvanced.hidden = advancedVisible; diff --git a/ext/js/pages/settings/scan-inputs-simple-controller.js b/ext/js/pages/settings/scan-inputs-simple-controller.js index 8d52af61..ddb68825 100644 --- a/ext/js/pages/settings/scan-inputs-simple-controller.js +++ b/ext/js/pages/settings/scan-inputs-simple-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {HotkeyUtil} from '../../input/hotkey-util.js'; import {yomitan} from '../../yomitan.js'; import {ScanInputsController} from './scan-inputs-controller.js'; @@ -27,10 +28,10 @@ export class ScanInputsSimpleController { constructor(settingsController) { /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; - /** @type {?HTMLInputElement} */ - this._middleMouseButtonScan = null; - /** @type {?HTMLSelectElement} */ - this._mainScanModifierKeyInput = null; + /** @type {HTMLInputElement} */ + this._middleMouseButtonScan = querySelectorNotNull(document, '#middle-mouse-button-scan'); + /** @type {HTMLSelectElement} */ + this._mainScanModifierKeyInput = querySelectorNotNull(document, '#main-scan-modifier-key'); /** @type {boolean} */ this._mainScanModifierKeyInputHasOther = false; /** @type {HotkeyUtil} */ @@ -39,9 +40,6 @@ export class ScanInputsSimpleController { /** */ async prepare() { - this._middleMouseButtonScan = /** @type {HTMLInputElement} */ (document.querySelector('#middle-mouse-button-scan')); - this._mainScanModifierKeyInput = /** @type {HTMLSelectElement} */ (document.querySelector('#main-scan-modifier-key')); - const {platform: {os}} = await yomitan.api.getEnvironmentInfo(); this._hotkeyUtil.os = os; diff --git a/ext/js/pages/settings/secondary-search-dictionary-controller.js b/ext/js/pages/settings/secondary-search-dictionary-controller.js index b20bd475..f24f6ea3 100644 --- a/ext/js/pages/settings/secondary-search-dictionary-controller.js +++ b/ext/js/pages/settings/secondary-search-dictionary-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class SecondarySearchDictionaryController { @@ -32,14 +33,12 @@ export class SecondarySearchDictionaryController { this._dictionaryInfoMap = new Map(); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); - /** @type {?HTMLElement} */ - this._container = null; + /** @type {HTMLElement} */ + this._container = querySelectorNotNull(document, '#secondary-search-dictionary-list'); } /** */ async prepare() { - this._container = /** @type {HTMLElement} */ (document.querySelector('#secondary-search-dictionary-list')); - await this._onDatabaseUpdated(); yomitan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); @@ -83,13 +82,16 @@ export class SecondarySearchDictionaryController { const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('secondary-search-dictionary')); fragment.appendChild(node); - const nameNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-title')); + /** @type {HTMLElement} */ + const nameNode = querySelectorNotNull(node, '.dictionary-title'); nameNode.textContent = name; - const versionNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-version')); + /** @type {HTMLElement} */ + const versionNode = querySelectorNotNull(node, '.dictionary-version'); versionNode.textContent = `rev.${dictionaryInfo.revision}`; - const toggle = /** @type {HTMLElement} */ (node.querySelector('.dictionary-allow-secondary-searches')); + /** @type {HTMLElement} */ + const toggle = querySelectorNotNull(node, '.dictionary-allow-secondary-searches'); toggle.dataset.setting = `dictionaries[${i}].allowSecondarySearches`; this._eventListeners.addEventListener(toggle, 'settingChanged', this._onEnabledChanged.bind(this, node), false); } diff --git a/ext/js/pages/settings/sentence-termination-characters-controller.js b/ext/js/pages/settings/sentence-termination-characters-controller.js index 80c4cdbe..7fd90b28 100644 --- a/ext/js/pages/settings/sentence-termination-characters-controller.js +++ b/ext/js/pages/settings/sentence-termination-characters-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; export class SentenceTerminationCharactersController { /** @@ -27,16 +28,16 @@ export class SentenceTerminationCharactersController { this._settingsController = settingsController; /** @type {SentenceTerminationCharacterEntry[]} */ this._entries = []; - /** @type {?HTMLButtonElement} */ - this._addButton = null; - /** @type {?HTMLButtonElement} */ - this._resetButton = null; - /** @type {?HTMLElement} */ - this._listTable = null; - /** @type {?HTMLElement} */ - this._listContainer = null; - /** @type {?HTMLElement} */ - this._emptyIndicator = null; + /** @type {HTMLButtonElement} */ + this._addButton = querySelectorNotNull(document, '#sentence-termination-character-list-add'); + /** @type {HTMLButtonElement} */ + this._resetButton = querySelectorNotNull(document, '#sentence-termination-character-list-reset'); + /** @type {HTMLElement} */ + this._listTable = querySelectorNotNull(document, '#sentence-termination-character-list-table'); + /** @type {HTMLElement} */ + this._listContainer = querySelectorNotNull(document, '#sentence-termination-character-list'); + /** @type {HTMLElement} */ + this._emptyIndicator = querySelectorNotNull(document, '#sentence-termination-character-list-empty'); } /** @type {import('./settings-controller.js').SettingsController} */ @@ -46,12 +47,6 @@ export class SentenceTerminationCharactersController { /** */ async prepare() { - this._addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#sentence-termination-character-list-add')); - this._resetButton = /** @type {HTMLButtonElement} */ (document.querySelector('#sentence-termination-character-list-reset')); - this._listTable = /** @type {HTMLElement} */ (document.querySelector('#sentence-termination-character-list-table')); - this._listContainer = /** @type {HTMLElement} */ (document.querySelector('#sentence-termination-character-list')); - this._emptyIndicator = /** @type {HTMLElement} */ (document.querySelector('#sentence-termination-character-list-empty')); - this._addButton.addEventListener('click', this._onAddClick.bind(this)); this._resetButton.addEventListener('click', this._onResetClick.bind(this)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); @@ -209,13 +204,20 @@ class SentenceTerminationCharacterEntry { const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} = this._data; const node = this._node; - const enabledToggle = /** @type {HTMLInputElement} */ (node.querySelector('.sentence-termination-character-enabled')); - const typeSelect = /** @type {HTMLSelectElement} */ (node.querySelector('.sentence-termination-character-type')); - const character1Input = /** @type {HTMLInputElement} */ (node.querySelector('.sentence-termination-character-input1')); - const character2Input = /** @type {HTMLInputElement} */ (node.querySelector('.sentence-termination-character-input2')); - const includeAtStartCheckbox = /** @type {HTMLInputElement} */ (node.querySelector('.sentence-termination-character-include-at-start')); - const includeAtEndheckbox = /** @type {HTMLInputElement} */ (node.querySelector('.sentence-termination-character-include-at-end')); - const menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.sentence-termination-character-entry-button')); + /** @type {HTMLInputElement} */ + const enabledToggle = querySelectorNotNull(node, '.sentence-termination-character-enabled'); + /** @type {HTMLSelectElement} */ + const typeSelect = querySelectorNotNull(node, '.sentence-termination-character-type'); + /** @type {HTMLInputElement} */ + const character1Input = querySelectorNotNull(node, '.sentence-termination-character-input1'); + /** @type {HTMLInputElement} */ + const character2Input = querySelectorNotNull(node, '.sentence-termination-character-input2'); + /** @type {HTMLInputElement} */ + const includeAtStartCheckbox = querySelectorNotNull(node, '.sentence-termination-character-include-at-start'); + /** @type {HTMLInputElement} */ + const includeAtEndheckbox = querySelectorNotNull(node, '.sentence-termination-character-include-at-end'); + /** @type {HTMLButtonElement} */ + const menuButton = querySelectorNotNull(node, '.sentence-termination-character-entry-button'); this._character1Input = character1Input; this._character2Input = character2Input; diff --git a/ext/js/pages/settings/settings-display-controller.js b/ext/js/pages/settings/settings-display-controller.js index 16e6cfae..e575a1cb 100644 --- a/ext/js/pages/settings/settings-display-controller.js +++ b/ext/js/pages/settings/settings-display-controller.js @@ -18,6 +18,7 @@ import {DocumentUtil} from '../../dom/document-util.js'; import {PopupMenu} from '../../dom/popup-menu.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {SelectorObserver} from '../../dom/selector-observer.js'; export class SettingsDisplayController { @@ -30,10 +31,10 @@ export class SettingsDisplayController { this._settingsController = settingsController; /** @type {import('./modal-controller.js').ModalController} */ this._modalController = modalController; - /** @type {?HTMLElement} */ - this._contentNode = null; - /** @type {?HTMLElement} */ - this._menuContainer = null; + /** @type {HTMLElement} */ + this._contentNode = querySelectorNotNull(document, '.content'); + /** @type {HTMLElement} */ + this._menuContainer = querySelectorNotNull(document, '#popup-menus'); /** @type {(event: MouseEvent) => void} */ this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this); /** @type {(event: MouseEvent) => void} */ @@ -42,9 +43,6 @@ export class SettingsDisplayController { /** */ prepare() { - this._contentNode = /** @type {HTMLElement} */ (document.querySelector('.content')); - this._menuContainer = /** @type {HTMLElement} */ (document.querySelector('#popup-menus')); - const onFabButtonClick = this._onFabButtonClick.bind(this); for (const fabButton of /** @type {NodeListOf} */ (document.querySelectorAll('.fab-button'))) { fabButton.addEventListener('click', onFabButtonClick, false); @@ -156,7 +154,8 @@ export class SettingsDisplayController { const container = this._getMoreContainer(node); if (container === null) { return; } - const more = /** @type {?HTMLElement} */ (container.querySelector('.more')); + /** @type {?HTMLElement} */ + const more = container.querySelector('.more'); if (more === null) { return; } const moreVisible = more.hidden; diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index 1b9723e8..3f0dac3f 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -18,6 +18,7 @@ import {log} from '../../core.js'; import {DocumentFocusController} from '../../dom/document-focus-controller.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; import {ExtensionContentController} from '../common/extension-content-controller.js'; import {AnkiController} from './anki-controller.js'; @@ -65,7 +66,9 @@ async function setupGenericSettingsController(genericSettingController) { const extensionContentController = new ExtensionContentController(); extensionContentController.prepare(); - const statusFooter = new StatusFooter(/** @type {HTMLElement} */ (document.querySelector('.status-footer-container'))); + /** @type {HTMLElement} */ + const statusFooterElement = querySelectorNotNull(document, '.status-footer-container'); + const statusFooter = new StatusFooter(statusFooterElement); statusFooter.prepare(); /** @type {?number} */ diff --git a/ext/js/pages/settings/sort-frequency-dictionary-controller.js b/ext/js/pages/settings/sort-frequency-dictionary-controller.js index e7759d95..2c56f023 100644 --- a/ext/js/pages/settings/sort-frequency-dictionary-controller.js +++ b/ext/js/pages/settings/sort-frequency-dictionary-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class SortFrequencyDictionaryController { @@ -25,25 +26,20 @@ export class SortFrequencyDictionaryController { constructor(settingsController) { /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; - /** @type {?HTMLSelectElement} */ - this._sortFrequencyDictionarySelect = null; - /** @type {?HTMLSelectElement} */ - this._sortFrequencyDictionaryOrderSelect = null; - /** @type {?HTMLButtonElement} */ - this._sortFrequencyDictionaryOrderAutoButton = null; - /** @type {?HTMLElement} */ - this._sortFrequencyDictionaryOrderContainerNode = null; + /** @type {HTMLSelectElement} */ + this._sortFrequencyDictionarySelect = querySelectorNotNull(document, '#sort-frequency-dictionary'); + /** @type {HTMLSelectElement} */ + this._sortFrequencyDictionaryOrderSelect = querySelectorNotNull(document, '#sort-frequency-dictionary-order'); + /** @type {HTMLButtonElement} */ + this._sortFrequencyDictionaryOrderAutoButton = querySelectorNotNull(document, '#sort-frequency-dictionary-order-auto'); + /** @type {HTMLElement} */ + this._sortFrequencyDictionaryOrderContainerNode = querySelectorNotNull(document, '#sort-frequency-dictionary-order-container'); /** @type {?import('core').TokenObject} */ this._getDictionaryInfoToken = null; } /** */ async prepare() { - this._sortFrequencyDictionarySelect = /** @type {HTMLSelectElement} */ (document.querySelector('#sort-frequency-dictionary')); - this._sortFrequencyDictionaryOrderSelect = /** @type {HTMLSelectElement} */ (document.querySelector('#sort-frequency-dictionary-order')); - this._sortFrequencyDictionaryOrderAutoButton = /** @type {HTMLButtonElement} */ (document.querySelector('#sort-frequency-dictionary-order-auto')); - this._sortFrequencyDictionaryOrderContainerNode = /** @type {HTMLElement} */ (document.querySelector('#sort-frequency-dictionary-order-container')); - await this._onDatabaseUpdated(); yomitan.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); diff --git a/ext/js/pages/settings/status-footer.js b/ext/js/pages/settings/status-footer.js index a8f1a8c4..4830dbd5 100644 --- a/ext/js/pages/settings/status-footer.js +++ b/ext/js/pages/settings/status-footer.js @@ -17,6 +17,7 @@ */ import {PanelElement} from '../../dom/panel-element.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; export class StatusFooter extends PanelElement { /** @@ -28,12 +29,13 @@ export class StatusFooter extends PanelElement { closingAnimationDuration: 375 // Milliseconds; includes buffer }); /** @type {HTMLElement} */ - this._body = /** @type {HTMLElement} */ (node.querySelector('.status-footer')); + this._body = querySelectorNotNull(node, '.status-footer'); } /** */ prepare() { - const closeButton = /** @type {HTMLElement} */ (this._body.querySelector('.status-footer-header-close')); + /** @type {HTMLElement} */ + const closeButton = querySelectorNotNull(this._body, '.status-footer-header-close'); this.on('closeCompleted', this._onCloseCompleted.bind(this)); closeButton.addEventListener('click', this._onCloseClick.bind(this), false); } diff --git a/ext/js/pages/settings/storage-controller.js b/ext/js/pages/settings/storage-controller.js index 7f323b48..16e03786 100644 --- a/ext/js/pages/settings/storage-controller.js +++ b/ext/js/pages/settings/storage-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; export class StorageController { @@ -53,7 +54,8 @@ export class StorageController { this._storageUseInfiniteNodes = /** @type {NodeListOf} */ (document.querySelectorAll('.storage-use-infinite')); this._storageUseValidNodes = /** @type {NodeListOf} */ (document.querySelectorAll('.storage-use-valid')); this._storageUseInvalidNodes = /** @type {NodeListOf} */ (document.querySelectorAll('.storage-use-invalid')); - const storageRefreshButton = /** @type {HTMLButtonElement} */ (document.querySelector('#storage-refresh')); + /** @type {HTMLButtonElement} */ + const storageRefreshButton = querySelectorNotNull(document, '#storage-refresh'); storageRefreshButton.addEventListener('click', this._onStorageRefreshButtonClick.bind(this), false); yomitan.on('storageChanged', this._onStorageChanged.bind(this)); diff --git a/ext/js/pages/settings/translation-text-replacements-controller.js b/ext/js/pages/settings/translation-text-replacements-controller.js index 050db8d1..a54c3dd9 100644 --- a/ext/js/pages/settings/translation-text-replacements-controller.js +++ b/ext/js/pages/settings/translation-text-replacements-controller.js @@ -17,6 +17,7 @@ */ import {EventListenerCollection} from '../../core.js'; +import {querySelectorNotNull} from '../../dom/query-selector.js'; export class TranslationTextReplacementsController { /** @@ -25,16 +26,16 @@ export class TranslationTextReplacementsController { constructor(settingsController) { /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; - /** @type {?HTMLElement} */ - this._entryContainer = null; + /** @type {HTMLElement} */ + this._entryContainer = querySelectorNotNull(document, '#translation-text-replacement-list'); /** @type {TranslationTextReplacementsEntry[]} */ this._entries = []; } /** */ async prepare() { - this._entryContainer = /** @type {HTMLElement} */ (document.querySelector('#translation-text-replacement-list')); - const addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#translation-text-replacement-add')); + /** @type {HTMLButtonElement} */ + const addButton = querySelectorNotNull(document, '#translation-text-replacement-add'); addButton.addEventListener('click', this._onAdd.bind(this), false); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); @@ -179,12 +180,18 @@ class TranslationTextReplacementsEntry { /** */ prepare() { - const patternInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-pattern')); - const replacementInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-replacement')); - const ignoreCaseToggle = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-pattern-ignore-case')); - const menuButton = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-button')); - const testInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-test-input')); - const testOutput = /** @type {HTMLInputElement} */ (this._node.querySelector('.translation-text-replacement-test-output')); + /** @type {HTMLInputElement} */ + const patternInput = querySelectorNotNull(this._node, '.translation-text-replacement-pattern'); + /** @type {HTMLInputElement} */ + const replacementInput = querySelectorNotNull(this._node, '.translation-text-replacement-replacement'); + /** @type {HTMLInputElement} */ + const ignoreCaseToggle = querySelectorNotNull(this._node, '.translation-text-replacement-pattern-ignore-case'); + /** @type {HTMLInputElement} */ + const menuButton = querySelectorNotNull(this._node, '.translation-text-replacement-button'); + /** @type {HTMLInputElement} */ + const testInput = querySelectorNotNull(this._node, '.translation-text-replacement-test-input'); + /** @type {HTMLInputElement} */ + const testOutput = querySelectorNotNull(this._node, '.translation-text-replacement-test-output'); this._patternInput = patternInput; this._replacementInput = replacementInput; @@ -221,8 +228,12 @@ class TranslationTextReplacementsEntry { _onMenuOpen(e) { const bodyNode = e.detail.menu.bodyNode; const testVisible = this._isTestVisible(); - /** @type {HTMLElement} */ (bodyNode.querySelector('[data-menu-action=showTest]')).hidden = testVisible; - /** @type {HTMLElement} */ (bodyNode.querySelector('[data-menu-action=hideTest]')).hidden = !testVisible; + /** @type {HTMLElement} */ + const element1 = querySelectorNotNull(bodyNode, '[data-menu-action=showTest]'); + /** @type {HTMLElement} */ + const element2 = querySelectorNotNull(bodyNode, '[data-menu-action=hideTest]'); + element1.hidden = testVisible; + element2.hidden = !testVisible; } /** diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index c034aae1..d208e996 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -18,6 +18,7 @@ import {log} from '../core.js'; import {DocumentFocusController} from '../dom/document-focus-controller.js'; +import {querySelectorNotNull} from '../dom/query-selector.js'; import {yomitan} from '../yomitan.js'; import {ExtensionContentController} from './common/extension-content-controller.js'; import {DictionaryController} from './settings/dictionary-controller.js'; @@ -55,7 +56,9 @@ async function setupGenericSettingsController(genericSettingController) { const extensionContentController = new ExtensionContentController(); extensionContentController.prepare(); - const statusFooter = new StatusFooter(/** @type {HTMLElement} */ (document.querySelector('.status-footer-container'))); + /** @type {HTMLElement} */ + const statusFooterElement = querySelectorNotNull(document, '.status-footer-container'); + const statusFooter = new StatusFooter(statusFooterElement); statusFooter.prepare(); await yomitan.prepare(); -- cgit v1.2.3