diff options
Diffstat (limited to 'ext/js/display/search-display-controller.js')
-rw-r--r-- | ext/js/display/search-display-controller.js | 171 |
1 files changed, 150 insertions, 21 deletions
diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index e31bd239..76e7bebe 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -22,34 +22,63 @@ import {EventListenerCollection, invokeMessageHandler} from '../core.js'; import {yomitan} from '../yomitan.js'; export class SearchDisplayController { + /** + * @param {number|undefined} tabId + * @param {number|undefined} frameId + * @param {Display} display + * @param {DisplayAudio} displayAudio + * @param {JapaneseUtil} japaneseUtil + * @param {SearchPersistentStateController} searchPersistentStateController + */ constructor(tabId, frameId, display, displayAudio, japaneseUtil, searchPersistentStateController) { + /** @type {number|undefined} */ this._tabId = tabId; + /** @type {number|undefined} */ this._frameId = frameId; + /** @type {Display} */ this._display = display; + /** @type {DisplayAudio} */ this._displayAudio = displayAudio; + /** @type {SearchPersistentStateController} */ this._searchPersistentStateController = searchPersistentStateController; - this._searchButton = document.querySelector('#search-button'); - this._searchBackButton = document.querySelector('#search-back-button'); - this._queryInput = document.querySelector('#search-textbox'); - this._introElement = document.querySelector('#intro'); - this._clipboardMonitorEnableCheckbox = document.querySelector('#clipboard-monitor-enable'); - this._wanakanaEnableCheckbox = document.querySelector('#wanakana-enable'); + /** @type {HTMLButtonElement} */ + this._searchButton = /** @type {HTMLButtonElement} */ (document.querySelector('#search-button')); + /** @type {HTMLButtonElement} */ + this._searchBackButton = /** @type {HTMLButtonElement} */ (document.querySelector('#search-back-button')); + /** @type {HTMLTextAreaElement} */ + this._queryInput = /** @type {HTMLTextAreaElement} */ (document.querySelector('#search-textbox')); + /** @type {HTMLElement} */ + this._introElement = /** @type {HTMLElement} */ (document.querySelector('#intro')); + /** @type {HTMLInputElement} */ + this._clipboardMonitorEnableCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#clipboard-monitor-enable')); + /** @type {HTMLInputElement} */ + this._wanakanaEnableCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#wanakana-enable')); + /** @type {EventListenerCollection} */ this._queryInputEvents = new EventListenerCollection(); + /** @type {boolean} */ this._queryInputEventsSetup = false; + /** @type {boolean} */ this._wanakanaEnabled = false; + /** @type {boolean} */ this._wanakanaBound = false; + /** @type {boolean} */ this._introVisible = true; + /** @type {?number} */ this._introAnimationTimer = null; + /** @type {boolean} */ this._clipboardMonitorEnabled = false; + /** @type {ClipboardMonitor} */ this._clipboardMonitor = new ClipboardMonitor({ japaneseUtil, clipboardReader: { getText: yomitan.api.clipboardGet.bind(yomitan.api) } }); + /** @type {import('core').MessageHandlerMap} */ this._messageHandlers = new Map(); } + /** */ async prepare() { await this._display.updateOptions(); @@ -84,15 +113,22 @@ export class SearchDisplayController { this._clipboardMonitorEnableCheckbox.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this)); this._display.hotkeyHandler.on('keydownNonHotkey', this._onKeyDown.bind(this)); - this._onDisplayOptionsUpdated({options: this._display.getOptions()}); + const displayOptions = this._display.getOptions(); + if (displayOptions !== null) { + this._onDisplayOptionsUpdated({options: displayOptions}); + } } + /** + * @param {import('display').SearchMode} mode + */ setMode(mode) { - this._setMode(mode, true); + this._searchPersistentStateController.mode = mode; } // Actions + /** */ _onActionFocusSearchBox() { if (this._queryInput === null) { return; } this._queryInput.focus(); @@ -101,22 +137,37 @@ export class SearchDisplayController { // Messages + /** + * @param {{mode: import('display').SearchMode}} details + */ _onMessageSetMode({mode}) { - this._searchPersistentStateController.mode = mode; + this.setMode(mode); } + /** + * @returns {import('display').SearchMode} + */ _onMessageGetMode() { return this._searchPersistentStateController.mode; } // Private + /** + * @param {{action: string, params?: import('core').SerializableObject}} message + * @param {chrome.runtime.MessageSender} sender + * @param {(response?: unknown) => void} callback + * @returns {boolean} + */ _onMessage({action, params}, sender, callback) { const messageHandler = this._messageHandlers.get(action); if (typeof messageHandler === 'undefined') { return false; } return invokeMessageHandler(messageHandler, params, callback, sender); } + /** + * @param {KeyboardEvent} e + */ _onKeyDown(e) { const {activeElement} = document; if ( @@ -132,6 +183,7 @@ export class SearchDisplayController { } } + /** */ async _onOptionsUpdated() { await this._display.updateOptions(); const query = this._queryInput.value; @@ -140,15 +192,21 @@ export class SearchDisplayController { } } + /** + * @param {import('display').OptionsUpdatedEvent} details + */ _onDisplayOptionsUpdated({options}) { this._clipboardMonitorEnabled = options.clipboard.enableSearchPageMonitor; this._updateClipboardMonitorEnabled(); - const enableWanakana = !!this._display.getOptions().general.enableWanakana; + const enableWanakana = !!options.general.enableWanakana; this._wanakanaEnableCheckbox.checked = enableWanakana; this._setWanakanaEnabled(enableWanakana); } + /** + * @param {import('display').ContentUpdateStartEvent} details + */ _onContentUpdateStart({type, query}) { let animate = false; let valid = false; @@ -182,38 +240,54 @@ export class SearchDisplayController { this._setIntroVisible(!valid, animate); } + /** */ _onSearchInput() { this._updateSearchHeight(false); } + /** + * @param {KeyboardEvent} e + */ _onSearchKeydown(e) { if (e.isComposing) { return; } const {code} = e; if (!((code === 'Enter' || code === 'NumpadEnter') && !e.shiftKey)) { return; } // Search + const element = /** @type {HTMLElement} */ (e.currentTarget); e.preventDefault(); e.stopImmediatePropagation(); - this._display.blurElement(e.currentTarget); + this._display.blurElement(element); this._search(true, 'new', true, null); } + /** + * @param {MouseEvent} e + */ _onSearch(e) { e.preventDefault(); this._search(true, 'new', true, null); } + /** */ _onSearchBackButtonClick() { this._display.history.back(); } + /** */ _onCopy() { // ignore copy from search page - this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); + const selection = window.getSelection(); + this._clipboardMonitor.setPreviousText(selection !== null ? selection.toString().trim() : ''); } + /** + * @param {{text: string, animate?: boolean}} details + */ _onExternalSearchUpdate({text, animate=true}) { - const {clipboard: {autoSearchContent, maximumSearchLength}} = this._display.getOptions(); + const options = this._display.getOptions(); + if (options === null) { return; } + const {clipboard: {autoSearchContent, maximumSearchLength}} = options; if (text.length > maximumSearchLength) { text = text.substring(0, maximumSearchLength); } @@ -222,27 +296,41 @@ export class SearchDisplayController { this._search(animate, 'clear', autoSearchContent, ['clipboard']); } + /** + * @param {Event} e + */ _onWanakanaEnableChange(e) { - const value = e.target.checked; + const element = /** @type {HTMLInputElement} */ (e.target); + const value = element.checked; this._setWanakanaEnabled(value); - yomitan.api.modifySettings([{ + /** @type {import('settings-modifications').ScopedModificationSet} */ + const modification = { action: 'set', path: 'general.enableWanakana', value, scope: 'profile', optionsContext: this._display.getOptionsContext() - }], 'search'); + }; + yomitan.api.modifySettings([modification], 'search'); } + /** + * @param {Event} e + */ _onClipboardMonitorEnableChange(e) { - const enabled = e.target.checked; + const element = /** @type {HTMLInputElement} */ (e.target); + const enabled = element.checked; this._setClipboardMonitorEnabled(enabled); } + /** */ _onModeChange() { this._updateClipboardMonitorEnabled(); } + /** + * @param {boolean} enabled + */ _setWanakanaEnabled(enabled) { if (this._queryInputEventsSetup && this._wanakanaEnabled === enabled) { return; } @@ -267,6 +355,10 @@ export class SearchDisplayController { this._queryInputEventsSetup = true; } + /** + * @param {boolean} visible + * @param {boolean} animate + */ _setIntroVisible(visible, animate) { if (this._introVisible === visible) { return; @@ -290,6 +382,9 @@ export class SearchDisplayController { } } + /** + * @param {boolean} animate + */ _showIntro(animate) { if (animate) { const duration = 0.4; @@ -310,6 +405,9 @@ export class SearchDisplayController { } } + /** + * @param {boolean} animate + */ _hideIntro(animate) { if (animate) { const duration = 0.4; @@ -323,6 +421,9 @@ export class SearchDisplayController { this._introElement.style.height = '0'; } + /** + * @param {boolean} value + */ async _setClipboardMonitorEnabled(value) { let modify = true; if (value) { @@ -335,15 +436,18 @@ export class SearchDisplayController { if (!modify) { return; } - await yomitan.api.modifySettings([{ + /** @type {import('settings-modifications').ScopedModificationSet} */ + const modification = { action: 'set', path: 'clipboard.enableSearchPageMonitor', value, scope: 'profile', optionsContext: this._display.getOptionsContext() - }], 'search'); + }; + await yomitan.api.modifySettings([modification], 'search'); } + /** */ _updateClipboardMonitorEnabled() { const enabled = this._clipboardMonitorEnabled; this._clipboardMonitorEnableCheckbox.checked = enabled; @@ -354,6 +458,9 @@ export class SearchDisplayController { } } + /** + * @returns {boolean} + */ _canEnableClipboardMonitor() { switch (this._searchPersistentStateController.mode) { case 'popup': @@ -364,6 +471,10 @@ export class SearchDisplayController { } } + /** + * @param {string[]} permissions + * @returns {Promise<boolean>} + */ _requestPermissions(permissions) { return new Promise((resolve) => { chrome.permissions.request( @@ -376,15 +487,23 @@ export class SearchDisplayController { }); } + /** + * @param {boolean} animate + * @param {import('display').HistoryMode} historyMode + * @param {boolean} lookup + * @param {?import('settings').OptionsContextFlag[]} flags + */ _search(animate, historyMode, lookup, flags) { const query = this._queryInput.value; const depth = this._display.depth; const url = window.location.href; const documentTitle = document.title; + /** @type {import('settings').OptionsContext} */ const optionsContext = {depth, url}; if (flags !== null) { optionsContext.flags = flags; } + /** @type {import('display').ContentDetails} */ const details = { focus: false, historyMode, @@ -399,7 +518,7 @@ export class SearchDisplayController { documentTitle }, content: { - dictionaryEntries: null, + dictionaryEntries: void 0, animate, contentOrigin: { tabId: this._tabId, @@ -411,6 +530,9 @@ export class SearchDisplayController { this._display.setContent(details); } + /** + * @param {boolean} shrink + */ _updateSearchHeight(shrink) { const node = this._queryInput; if (shrink) { @@ -423,12 +545,19 @@ export class SearchDisplayController { } } + /** + * @param {import('core').MessageHandlerArray} handlers + */ _registerMessageHandlers(handlers) { for (const [name, handlerInfo] of handlers) { this._messageHandlers.set(name, handlerInfo); } } + /** + * @param {?Element} element + * @returns {boolean} + */ _isElementInput(element) { if (element === null) { return false; } switch (element.tagName.toLowerCase()) { @@ -438,7 +567,7 @@ export class SearchDisplayController { case 'select': return true; } - if (element.isContentEditable) { return true; } + if (element instanceof HTMLElement && element.isContentEditable) { return true; } return false; } } |