aboutsummaryrefslogtreecommitdiff
path: root/ext/js/display/search-display-controller.js
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-12-06 03:53:16 +0000
committerGitHub <noreply@github.com>2023-12-06 03:53:16 +0000
commitbd5bc1a5db29903bc098995cd9262c4576bf76af (patch)
treec9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/display/search-display-controller.js
parentfd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff)
parent23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff)
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/display/search-display-controller.js')
-rw-r--r--ext/js/display/search-display-controller.js171
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..2778c4cd 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 {import('./display.js').Display} display
+ * @param {import('./display-audio.js').DisplayAudio} displayAudio
+ * @param {import('../language/sandbox/japanese-util.js').JapaneseUtil} japaneseUtil
+ * @param {import('./search-persistent-state-controller.js').SearchPersistentStateController} searchPersistentStateController
+ */
constructor(tabId, frameId, display, displayAudio, japaneseUtil, searchPersistentStateController) {
+ /** @type {number|undefined} */
this._tabId = tabId;
+ /** @type {number|undefined} */
this._frameId = frameId;
+ /** @type {import('./display.js').Display} */
this._display = display;
+ /** @type {import('./display-audio.js').DisplayAudio} */
this._displayAudio = displayAudio;
+ /** @type {import('./search-persistent-state-controller.js').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 {?import('core').Timeout} */
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;
}
}