aboutsummaryrefslogtreecommitdiff
path: root/ext/js/pages
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/pages')
-rw-r--r--ext/js/pages/action-popup-main.js103
-rw-r--r--ext/js/pages/common/extension-content-controller.js32
-rw-r--r--ext/js/pages/info-main.js34
-rw-r--r--ext/js/pages/permissions-main.js41
-rw-r--r--ext/js/pages/settings/anki-controller.js427
-rw-r--r--ext/js/pages/settings/anki-templates-controller.js162
-rw-r--r--ext/js/pages/settings/audio-controller.js181
-rw-r--r--ext/js/pages/settings/backup-controller.js188
-rw-r--r--ext/js/pages/settings/collapsible-dictionary-controller.js86
-rw-r--r--ext/js/pages/settings/dictionary-controller.js327
-rw-r--r--ext/js/pages/settings/dictionary-import-controller.js151
-rw-r--r--ext/js/pages/settings/extension-keyboard-shortcuts-controller.js135
-rw-r--r--ext/js/pages/settings/generic-setting-controller.js181
-rw-r--r--ext/js/pages/settings/keyboard-mouse-input-field.js92
-rw-r--r--ext/js/pages/settings/keyboard-shortcuts-controller.js268
-rw-r--r--ext/js/pages/settings/mecab-controller.js37
-rw-r--r--ext/js/pages/settings/modal-controller.js12
-rw-r--r--ext/js/pages/settings/modal.js28
-rw-r--r--ext/js/pages/settings/nested-popups-controller.js47
-rw-r--r--ext/js/pages/settings/permissions-origin-controller.js73
-rw-r--r--ext/js/pages/settings/permissions-toggle-controller.js45
-rw-r--r--ext/js/pages/settings/persistent-storage-controller.js29
-rw-r--r--ext/js/pages/settings/popup-preview-controller.js32
-rw-r--r--ext/js/pages/settings/popup-preview-frame-main.js6
-rw-r--r--ext/js/pages/settings/popup-preview-frame.js102
-rw-r--r--ext/js/pages/settings/popup-window-controller.js7
-rw-r--r--ext/js/pages/settings/profile-conditions-ui.js391
-rw-r--r--ext/js/pages/settings/profile-controller.js287
-rw-r--r--ext/js/pages/settings/recommended-permissions-controller.js36
-rw-r--r--ext/js/pages/settings/scan-inputs-controller.js171
-rw-r--r--ext/js/pages/settings/scan-inputs-simple-controller.js90
-rw-r--r--ext/js/pages/settings/secondary-search-dictionary-controller.js40
-rw-r--r--ext/js/pages/settings/sentence-termination-characters-controller.js127
-rw-r--r--ext/js/pages/settings/settings-controller.js172
-rw-r--r--ext/js/pages/settings/settings-display-controller.js152
-rw-r--r--ext/js/pages/settings/settings-main.js8
-rw-r--r--ext/js/pages/settings/sort-frequency-dictionary-controller.js93
-rw-r--r--ext/js/pages/settings/status-footer.js34
-rw-r--r--ext/js/pages/settings/storage-controller.js67
-rw-r--r--ext/js/pages/settings/translation-text-replacements-controller.js107
-rw-r--r--ext/js/pages/welcome-main.js6
41 files changed, 3776 insertions, 831 deletions
diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js
index 32bfcb7f..94b9b356 100644
--- a/ext/js/pages/action-popup-main.js
+++ b/ext/js/pages/action-popup-main.js
@@ -22,10 +22,13 @@ import {yomitan} from '../yomitan.js';
export class DisplayController {
constructor() {
+ /** @type {?import('settings').Options} */
this._optionsFull = null;
+ /** @type {PermissionsUtil} */
this._permissionsUtil = new PermissionsUtil();
}
+ /** */
async prepare() {
const manifest = chrome.runtime.getManifest();
@@ -39,7 +42,12 @@ export class DisplayController {
this._setupHotkeys();
- const optionsPageUrl = manifest.options_ui.page;
+ const optionsPageUrl = (
+ typeof manifest.options_ui === 'object' &&
+ manifest.options_ui !== null &&
+ typeof manifest.options_ui.page === 'string' ?
+ manifest.options_ui.page : ''
+ );
this._setupButtonEvents('.action-open-settings', 'openSettingsPage', chrome.runtime.getURL(optionsPageUrl));
this._setupButtonEvents('.action-open-permissions', null, chrome.runtime.getURL('/permissions.html'));
@@ -49,7 +57,7 @@ export class DisplayController {
this._setupOptions(primaryProfile);
}
- document.querySelector('.action-select-profile').hidden = (profiles.length <= 1);
+ /** @type {HTMLElement} */ (document.querySelector('.action-select-profile')).hidden = (profiles.length <= 1);
this._updateProfileSelect(profiles, profileCurrent);
@@ -60,13 +68,18 @@ export class DisplayController {
// Private
+ /**
+ * @param {MouseEvent} e
+ */
_onSearchClick(e) {
if (!e.shiftKey) { return; }
e.preventDefault();
location.href = '/search.html?action-popup=true';
- return false;
}
+ /**
+ * @param {chrome.runtime.Manifest} manifest
+ */
_showExtensionInfo(manifest) {
const node = document.getElementById('extension-info');
if (node === null) { return; }
@@ -74,11 +87,21 @@ export class DisplayController {
node.textContent = `${manifest.name} v${manifest.version}`;
}
+ /**
+ * @param {string} selector
+ * @param {?string} command
+ * @param {string} url
+ * @param {(event: MouseEvent) => void} [customHandler]
+ */
_setupButtonEvents(selector, command, url, customHandler) {
+ /** @type {NodeListOf<HTMLAnchorElement>} */
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
if (typeof command === 'string') {
- node.addEventListener('click', (e) => {
+ /**
+ * @param {MouseEvent} e
+ */
+ const onClick = (e) => {
if (e.button !== 0) { return; }
if (typeof customHandler === 'function') {
const result = customHandler(e);
@@ -86,12 +109,17 @@ export class DisplayController {
}
yomitan.api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
e.preventDefault();
- }, false);
- node.addEventListener('auxclick', (e) => {
+ };
+ /**
+ * @param {MouseEvent} e
+ */
+ const onAuxClick = (e) => {
if (e.button !== 1) { return; }
yomitan.api.commandExec(command, {mode: 'newTab'});
e.preventDefault();
- }, false);
+ };
+ node.addEventListener('click', onClick, false);
+ node.addEventListener('auxclick', onAuxClick, false);
}
if (typeof url === 'string') {
@@ -102,6 +130,7 @@ export class DisplayController {
}
}
+ /** */
async _setupEnvironment() {
const urlSearchParams = new URLSearchParams(location.search);
let mode = urlSearchParams.get('mode');
@@ -129,6 +158,9 @@ export class DisplayController {
document.documentElement.dataset.mode = mode;
}
+ /**
+ * @returns {Promise<chrome.tabs.Tab|undefined>}
+ */
_getCurrentTab() {
return new Promise((resolve, reject) => {
chrome.tabs.getCurrent((result) => {
@@ -142,10 +174,13 @@ export class DisplayController {
});
}
+ /**
+ * @param {import('settings').Profile} profile
+ */
_setupOptions({options}) {
const extensionEnabled = options.general.enable;
const onToggleChanged = () => yomitan.api.commandExec('toggleTextScanning');
- for (const toggle of document.querySelectorAll('#enable-search,#enable-search2')) {
+ for (const toggle of /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('#enable-search,#enable-search2'))) {
toggle.checked = extensionEnabled;
toggle.addEventListener('change', onToggleChanged, false);
}
@@ -153,11 +188,12 @@ export class DisplayController {
this._updatePermissionsWarnings(options);
}
+ /** */
async _setupHotkeys() {
const hotkeyHelpController = new HotkeyHelpController();
await hotkeyHelpController.prepare();
- const {profiles, profileCurrent} = this._optionsFull;
+ const {profiles, profileCurrent} = /** @type {import('settings').Options} */ (this._optionsFull);
const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null;
if (primaryProfile !== null) {
hotkeyHelpController.setOptions(primaryProfile.options);
@@ -166,9 +202,13 @@ export class DisplayController {
hotkeyHelpController.setupNode(document.documentElement);
}
+ /**
+ * @param {import('settings').Profile[]} profiles
+ * @param {number} profileCurrent
+ */
_updateProfileSelect(profiles, profileCurrent) {
- const select = document.querySelector('#profile-select');
- const optionGroup = document.querySelector('#profile-select-option-group');
+ const select = /** @type {HTMLSelectElement} */ (document.querySelector('#profile-select'));
+ const optionGroup = /** @type {HTMLElement} */ (document.querySelector('#profile-select-option-group'));
const fragment = document.createDocumentFragment();
for (let i = 0, ii = profiles.length; i < ii; ++i) {
const {name} = profiles[i];
@@ -184,26 +224,37 @@ export class DisplayController {
select.addEventListener('change', this._onProfileSelectChange.bind(this), false);
}
- _onProfileSelectChange(e) {
- const value = parseInt(e.currentTarget.value, 10);
- if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= this._optionsFull.profiles.length) {
+ /**
+ * @param {Event} event
+ */
+ _onProfileSelectChange(event) {
+ const node = /** @type {HTMLInputElement} */ (event.currentTarget);
+ const value = parseInt(node.value, 10);
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= /** @type {import('settings').Options} */ (this._optionsFull).profiles.length) {
this._setPrimaryProfileIndex(value);
}
}
+ /**
+ * @param {number} value
+ */
async _setPrimaryProfileIndex(value) {
- return await yomitan.api.modifySettings(
- [{
- action: 'set',
- path: 'profileCurrent',
- value,
- scope: 'global'
- }]
- );
+ /** @type {import('settings-modifications').ScopedModificationSet} */
+ const modification = {
+ action: 'set',
+ path: 'profileCurrent',
+ value,
+ scope: 'global',
+ optionsContext: null
+ };
+ await yomitan.api.modifySettings([modification], 'action-popup');
}
+ /**
+ * @param {import('settings').ProfileOptions} options
+ */
async _updateDictionariesEnabledWarnings(options) {
- const noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning');
+ const noDictionariesEnabledWarnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.no-dictionaries-enabled-warning'));
const dictionaries = await yomitan.api.getDictionaryInfo();
const enabledDictionaries = new Set();
@@ -226,16 +277,20 @@ export class DisplayController {
}
}
+ /**
+ * @param {import('settings').ProfileOptions} options
+ */
async _updatePermissionsWarnings(options) {
const permissions = await this._permissionsUtil.getAllPermissions();
if (this._permissionsUtil.hasRequiredPermissionsForOptions(permissions, options)) { return; }
- const warnings = document.querySelectorAll('.action-open-permissions,.permissions-required-warning');
+ const warnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.action-open-permissions,.permissions-required-warning'));
for (const node of warnings) {
node.hidden = false;
}
}
+ /** @returns {Promise<boolean>} */
async _isSafari() {
const {browser} = await yomitan.api.getEnvironmentInfo();
return browser === 'safari';
diff --git a/ext/js/pages/common/extension-content-controller.js b/ext/js/pages/common/extension-content-controller.js
index 3792130c..1c3f9c74 100644
--- a/ext/js/pages/common/extension-content-controller.js
+++ b/ext/js/pages/common/extension-content-controller.js
@@ -19,6 +19,7 @@
import {Environment} from '../../extension/environment.js';
export class ExtensionContentController {
+ /** */
prepare() {
this._prepareSpecialUrls();
this._prepareExtensionIdExamples();
@@ -27,6 +28,7 @@ export class ExtensionContentController {
// Private
+ /** */
async _prepareEnvironmentInfo() {
const {dataset} = document.documentElement;
const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
@@ -40,6 +42,7 @@ export class ExtensionContentController {
dataset.os = platform.os;
}
+ /** */
_prepareExtensionIdExamples() {
const nodes = document.querySelectorAll('.extension-id-example');
let url = '';
@@ -53,8 +56,9 @@ export class ExtensionContentController {
}
}
+ /** */
_prepareSpecialUrls() {
- const nodes = document.querySelectorAll('[data-special-url]');
+ const nodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('[data-special-url]'));
if (nodes.length === 0) { return; }
let extensionId = '';
@@ -77,16 +81,27 @@ export class ExtensionContentController {
}
}
+ /**
+ * @param {MouseEvent} e
+ */
_onSpecialUrlLinkClick(e) {
switch (e.button) {
case 0:
case 1:
- e.preventDefault();
- this._createTab(e.currentTarget.dataset.specialUrl, true);
+ {
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
+ const {specialUrl} = element.dataset;
+ if (typeof specialUrl !== 'string') { return; }
+ e.preventDefault();
+ this._createTab(specialUrl, true);
+ }
break;
}
}
+ /**
+ * @param {MouseEvent} e
+ */
_onSpecialUrlLinkMouseDown(e) {
switch (e.button) {
case 0:
@@ -96,10 +111,17 @@ export class ExtensionContentController {
}
}
+ /**
+ * @param {string} url
+ * @param {boolean} useOpener
+ * @returns {Promise<chrome.tabs.Tab>}
+ */
async _createTab(url, useOpener) {
+ /** @type {number|undefined} */
let openerTabId;
if (useOpener) {
try {
+ /** @type {chrome.tabs.Tab|undefined} */
const tab = await new Promise((resolve, reject) => {
chrome.tabs.getCurrent((result) => {
const e = chrome.runtime.lastError;
@@ -110,7 +132,9 @@ export class ExtensionContentController {
}
});
});
- openerTabId = tab.id;
+ if (typeof tab !== 'undefined') {
+ openerTabId = tab.id;
+ }
} catch (e) {
// NOP
}
diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js
index 7c6bc993..f71d64c3 100644
--- a/ext/js/pages/info-main.js
+++ b/ext/js/pages/info-main.js
@@ -22,6 +22,10 @@ import {yomitan} from '../yomitan.js';
import {BackupController} from './settings/backup-controller.js';
import {SettingsController} from './settings/settings-controller.js';
+/**
+ * @param {import('environment').Browser} browser
+ * @returns {string}
+ */
function getBrowserDisplayName(browser) {
switch (browser) {
case 'chrome': return 'Chrome';
@@ -29,10 +33,15 @@ function getBrowserDisplayName(browser) {
case 'firefox-mobile': return 'Firefox for Android';
case 'edge': return 'Edge';
case 'edge-legacy': return 'Edge Legacy';
+ case 'safari': return 'Safari';
default: return `${browser}`;
}
}
+/**
+ * @param {import('environment').OperatingSystem} os
+ * @returns {string}
+ */
function getOperatingSystemDisplayName(os) {
switch (os) {
case 'mac': return 'Mac OS';
@@ -60,14 +69,15 @@ function getOperatingSystemDisplayName(os) {
const {name, version} = manifest;
const {browser, platform: {os}} = await yomitan.api.getEnvironmentInfo();
- const thisVersionLink = document.querySelector('#release-notes-this-version-link');
- thisVersionLink.href = thisVersionLink.dataset.hrefFormat.replace(/\{version\}/g, version);
+ const thisVersionLink = /** @type {HTMLLinkElement} */ (document.querySelector('#release-notes-this-version-link'));
+ const {hrefFormat} = thisVersionLink.dataset;
+ thisVersionLink.href = typeof hrefFormat === 'string' ? hrefFormat.replace(/\{version\}/g, version) : '';
- document.querySelector('#version').textContent = `${name} ${version}`;
- document.querySelector('#browser').textContent = getBrowserDisplayName(browser);
- document.querySelector('#platform').textContent = getOperatingSystemDisplayName(os);
- document.querySelector('#language').textContent = `${language}`;
- document.querySelector('#user-agent').textContent = userAgent;
+ /** @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;
(async () => {
let ankiConnectVersion = null;
@@ -77,9 +87,9 @@ function getOperatingSystemDisplayName(os) {
// NOP
}
- document.querySelector('#anki-connect-version').textContent = (ankiConnectVersion !== null ? `${ankiConnectVersion}` : 'Unknown');
- document.querySelector('#anki-connect-version-container').hasError = `${ankiConnectVersion === null}`;
- document.querySelector('#anki-connect-version-unknown-message').hidden = (ankiConnectVersion !== null);
+ /** @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);
})();
(async () => {
@@ -105,8 +115,8 @@ function getOperatingSystemDisplayName(os) {
fragment.appendChild(node);
}
- document.querySelector('#installed-dictionaries-none').hidden = (dictionaryInfos.length !== 0);
- const container = document.querySelector('#installed-dictionaries');
+ /** @type {HTMLElement} */ (document.querySelector('#installed-dictionaries-none')).hidden = (dictionaryInfos.length !== 0);
+ const container = /** @type {HTMLElement} */ (document.querySelector('#installed-dictionaries'));
container.textContent = '';
container.appendChild(fragment);
})();
diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js
index ff614880..064e9240 100644
--- a/ext/js/pages/permissions-main.js
+++ b/ext/js/pages/permissions-main.js
@@ -27,6 +27,9 @@ import {PersistentStorageController} from './settings/persistent-storage-control
import {SettingsController} from './settings/settings-controller.js';
import {SettingsDisplayController} from './settings/settings-display-controller.js';
+/**
+ * @returns {Promise<void>}
+ */
async function setupEnvironmentInfo() {
const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
const {browser, platform} = await yomitan.api.getEnvironmentInfo();
@@ -35,20 +38,39 @@ async function setupEnvironmentInfo() {
document.documentElement.dataset.manifestVersion = `${manifestVersion}`;
}
+/**
+ * @returns {Promise<boolean>}
+ */
async function isAllowedIncognitoAccess() {
return await new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
}
+/**
+ * @returns {Promise<boolean>}
+ */
async function isAllowedFileSchemeAccess() {
return await new Promise((resolve) => chrome.extension.isAllowedFileSchemeAccess(resolve));
}
+/**
+ * @returns {void}
+ */
function setupPermissionsToggles() {
const manifest = chrome.runtime.getManifest();
- let optionalPermissions = manifest.optional_permissions;
- if (!Array.isArray(optionalPermissions)) { optionalPermissions = []; }
- optionalPermissions = new Set(optionalPermissions);
+ const optionalPermissions = manifest.optional_permissions;
+ /** @type {Set<string>} */
+ const optionalPermissionsSet = new Set(optionalPermissions);
+ if (Array.isArray(optionalPermissions)) {
+ for (const permission of optionalPermissions) {
+ optionalPermissionsSet.add(permission);
+ }
+ }
+ /**
+ * @param {Set<string>} set
+ * @param {string[]} values
+ * @returns {boolean}
+ */
const hasAllPermisions = (set, values) => {
for (const value of values) {
if (!set.has(value)) { return false; }
@@ -56,10 +78,10 @@ function setupPermissionsToggles() {
return true;
};
- for (const toggle of document.querySelectorAll('.permissions-toggle')) {
- let permissions = toggle.dataset.requiredPermissions;
- permissions = (typeof permissions === 'string' && permissions.length > 0 ? permissions.split(' ') : []);
- toggle.disabled = !hasAllPermisions(optionalPermissions, permissions);
+ for (const toggle of /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('.permissions-toggle'))) {
+ const permissions = toggle.dataset.requiredPermissions;
+ const permissionsArray = (typeof permissions === 'string' && permissions.length > 0 ? permissions.split(' ') : []);
+ toggle.disabled = !hasAllPermisions(optionalPermissionsSet, permissionsArray);
}
}
@@ -77,9 +99,10 @@ function setupPermissionsToggles() {
setupEnvironmentInfo();
+ /** @type {[HTMLInputElement, HTMLInputElement]} */
const permissionsCheckboxes = [
- document.querySelector('#permission-checkbox-allow-in-private-windows'),
- document.querySelector('#permission-checkbox-allow-file-url-access')
+ /** @type {HTMLInputElement} */ (document.querySelector('#permission-checkbox-allow-in-private-windows')),
+ /** @type {HTMLInputElement} */ (document.querySelector('#permission-checkbox-allow-file-url-access'))
];
const permissions = await Promise.all([
diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js
index 8164b8f6..05bfdadc 100644
--- a/ext/js/pages/settings/anki-controller.js
+++ b/ext/js/pages/settings/anki-controller.js
@@ -18,15 +18,22 @@
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 {SelectorObserver} from '../../dom/selector-observer.js';
import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js';
import {yomitan} from '../../yomitan.js';
export class AnkiController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {AnkiConnect} */
this._ankiConnect = new AnkiConnect();
+ /** @type {SelectorObserver<AnkiCardController>} */
this._selectorObserver = new SelectorObserver({
selector: '.anki-card',
ignoreSelector: null,
@@ -34,52 +41,74 @@ export class AnkiController {
onRemoved: this._removeCardController.bind(this),
isStale: this._isCardControllerStale.bind(this)
});
+ /** @type {Intl.Collator} */
this._stringComparer = new Intl.Collator(); // Locale does not matter
+ /** @type {?Promise<import('anki-controller').AnkiData>} */
this._getAnkiDataPromise = null;
+ /** @type {?HTMLElement} */
this._ankiErrorContainer = null;
+ /** @type {?HTMLElement} */
this._ankiErrorMessageNode = null;
+ /** @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;
+ /** @type {?Error} */
this._ankiError = null;
+ /** @type {?import('core').TokenObject} */
this._validateFieldsToken = null;
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._settingsController;
}
+ /** */
async prepare() {
- this._ankiErrorContainer = document.querySelector('#anki-error');
- this._ankiErrorMessageNode = document.querySelector('#anki-error-message');
- this._ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent;
- this._ankiErrorMessageDetailsNode = document.querySelector('#anki-error-message-details');
- this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details-container');
- this._ankiErrorMessageDetailsToggle = document.querySelector('#anki-error-message-details-toggle');
- this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');
- this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');
- this._ankiCardPrimary = document.querySelector('#anki-card-primary');
- const ankiApiKeyInput = document.querySelector('#anki-api-key-input');
- const ankiCardPrimaryTypeRadios = document.querySelectorAll('input[type=radio][name=anki-card-primary-type]');
+ 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'));
+ const ankiCardPrimaryTypeRadios = /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('input[type=radio][name=anki-card-primary-type]'));
+ const ankiErrorLog = /** @type {HTMLElement} */ (document.querySelector('#anki-error-log'));
this._setupFieldMenus();
this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false);
- if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); }
+ if (this._ankiEnableCheckbox !== null) {
+ this._ankiEnableCheckbox.addEventListener(
+ /** @type {string} */ ('settingChanged'),
+ /** @type {EventListener} */ (this._onAnkiEnableChanged.bind(this)),
+ false
+ );
+ }
for (const input of ankiCardPrimaryTypeRadios) {
input.addEventListener('change', this._onAnkiCardPrimaryTypeRadioChange.bind(this), false);
}
- const testAnkiNoteViewerButtons = document.querySelectorAll('.test-anki-note-viewer-button');
+ const testAnkiNoteViewerButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.test-anki-note-viewer-button'));
const onTestAnkiNoteViewerButtonClick = this._onTestAnkiNoteViewerButtonClick.bind(this);
for (const button of testAnkiNoteViewerButtons) {
button.addEventListener('click', onTestAnkiNoteViewerButtonClick, false);
}
- document.querySelector('#anki-error-log').addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this));
+ ankiErrorLog.addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this));
ankiApiKeyInput.addEventListener('focus', this._onApiKeyInputFocus.bind(this));
ankiApiKeyInput.addEventListener('blur', this._onApiKeyInputBlur.bind(this));
@@ -94,6 +123,10 @@ export class AnkiController {
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
}
+ /**
+ * @param {string} type
+ * @returns {string[]}
+ */
getFieldMarkers(type) {
switch (type) {
case 'terms':
@@ -154,6 +187,9 @@ export class AnkiController {
}
}
+ /**
+ * @returns {Promise<import('anki-controller').AnkiData>}
+ */
async getAnkiData() {
let promise = this._getAnkiDataPromise;
if (promise === null) {
@@ -164,23 +200,37 @@ export class AnkiController {
return promise;
}
+ /**
+ * @param {string} model
+ * @returns {Promise<string[]>}
+ */
async getModelFieldNames(model) {
return await this._ankiConnect.getModelFieldNames(model);
}
+ /**
+ * @param {string} fieldValue
+ * @returns {string[]}
+ */
getRequiredPermissions(fieldValue) {
return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
}
// Private
+ /** */
async _updateOptions() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
async _onOptionsChanged({options: {anki}}) {
- let {apiKey} = anki;
+ /** @type {?string} */
+ let apiKey = anki.apiKey;
if (apiKey === '') { apiKey = null; }
this._ankiConnect.server = anki.server;
this._ankiConnect.enabled = anki.enable;
@@ -190,44 +240,73 @@ export class AnkiController {
this._selectorObserver.observe(document.documentElement, true);
}
+ /** */
_onAnkiErrorMessageDetailsToggleClick() {
- const node = this._ankiErrorMessageDetailsContainer;
+ const node = /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsContainer);
node.hidden = !node.hidden;
}
+ /**
+ * @param {import('dom-data-binder').SettingChangedEvent} event
+ */
_onAnkiEnableChanged({detail: {value}}) {
if (this._ankiConnect.server === null) { return; }
- this._ankiConnect.enabled = value;
+ this._ankiConnect.enabled = typeof value === 'boolean' && value;
for (const cardController of this._selectorObserver.datas()) {
cardController.updateAnkiState();
}
}
+ /**
+ * @param {Event} e
+ */
_onAnkiCardPrimaryTypeRadioChange(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
if (!node.checked) { return; }
-
- this._setAnkiCardPrimaryType(node.dataset.value, node.dataset.ankiCardMenu);
+ const {value, ankiCardMenu} = node.dataset;
+ if (typeof value !== 'string') { return; }
+ this._setAnkiCardPrimaryType(value, ankiCardMenu);
}
+ /** */
_onAnkiErrorLogLinkClick() {
if (this._ankiError === null) { return; }
console.log({error: this._ankiError});
}
+ /**
+ * @param {MouseEvent} e
+ */
_onTestAnkiNoteViewerButtonClick(e) {
- this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode);
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
+ const {mode} = element.dataset;
+ if (typeof mode !== 'string') { return; }
+ const mode2 = this._normalizeAnkiNoteGuiMode(mode);
+ if (mode2 === null) { return; }
+ this._testAnkiNoteViewerSafe(mode2);
}
+ /**
+ * @param {Event} e
+ */
_onApiKeyInputFocus(e) {
- e.currentTarget.type = 'text';
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ element.type = 'text';
}
+ /**
+ * @param {Event} e
+ */
_onApiKeyInputBlur(e) {
- e.currentTarget.type = 'password';
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ element.type = 'password';
}
+ /**
+ * @param {string} ankiCardType
+ * @param {string} [ankiCardMenu]
+ */
_setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {
if (this._ankiCardPrimary === null) { return; }
this._ankiCardPrimary.dataset.ankiCardType = ankiCardType;
@@ -238,28 +317,43 @@ export class AnkiController {
}
}
+ /**
+ * @param {Element} node
+ * @returns {AnkiCardController}
+ */
_createCardController(node) {
- const cardController = new AnkiCardController(this._settingsController, this, node);
+ const cardController = new AnkiCardController(this._settingsController, this, /** @type {HTMLElement} */ (node));
cardController.prepare();
return cardController;
}
- _removeCardController(node, cardController) {
+ /**
+ * @param {Element} _node
+ * @param {AnkiCardController} cardController
+ */
+ _removeCardController(_node, cardController) {
cardController.cleanup();
}
- _isCardControllerStale(node, cardController) {
+ /**
+ * @param {Element} _node
+ * @param {AnkiCardController} cardController
+ * @returns {boolean}
+ */
+ _isCardControllerStale(_node, cardController) {
return cardController.isStale();
}
+ /** */
_setupFieldMenus() {
+ /** @type {[types: string[], selector: string][]} */
const fieldMenuTargets = [
[['terms'], '#anki-card-terms-field-menu-template'],
[['kanji'], '#anki-card-kanji-field-menu-template'],
[['terms', 'kanji'], '#anki-card-all-field-menu-template']
];
for (const [types, selector] of fieldMenuTargets) {
- const element = document.querySelector(selector);
+ const element = /** @type {HTMLTemplateElement} */ (document.querySelector(selector));
if (element === null) { continue; }
let markers = [];
@@ -284,6 +378,9 @@ export class AnkiController {
}
}
+ /**
+ * @returns {Promise<import('anki-controller').AnkiData>}
+ */
async _getAnkiData() {
this._setAnkiStatusChanging();
const [
@@ -305,85 +402,108 @@ export class AnkiController {
return {deckNames, modelNames};
}
+ /**
+ * @returns {Promise<[deckNames: string[], error: ?Error]>}
+ */
async _getDeckNames() {
try {
const result = await this._ankiConnect.getDeckNames();
this._sortStringArray(result);
return [result, null];
} catch (e) {
- return [[], e];
+ return [[], e instanceof Error ? e : new Error(`${e}`)];
}
}
+ /**
+ * @returns {Promise<[modelNames: string[], error: ?Error]>}
+ */
async _getModelNames() {
try {
const result = await this._ankiConnect.getModelNames();
this._sortStringArray(result);
return [result, null];
} catch (e) {
- return [[], e];
+ return [[], e instanceof Error ? e : new Error(`${e}`)];
}
}
+ /** */
_setAnkiStatusChanging() {
- this._ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent;
- this._ankiErrorMessageNode.classList.remove('danger-text');
+ const ankiErrorMessageNode = /** @type {HTMLElement} */ (this._ankiErrorMessageNode);
+ ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent;
+ ankiErrorMessageNode.classList.remove('danger-text');
}
+ /** */
_hideAnkiError() {
+ const ankiErrorMessageNode = /** @type {HTMLElement} */ (this._ankiErrorMessageNode);
if (this._ankiErrorContainer !== null) {
this._ankiErrorContainer.hidden = true;
}
- this._ankiErrorMessageDetailsContainer.hidden = true;
- this._ankiErrorMessageDetailsToggle.hidden = true;
- this._ankiErrorInvalidResponseInfo.hidden = true;
- this._ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled');
- this._ankiErrorMessageNode.classList.remove('danger-text');
- this._ankiErrorMessageDetailsNode.textContent = '';
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsContainer).hidden = true;
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsToggle).hidden = true;
+ /** @type {HTMLElement} */ (this._ankiErrorInvalidResponseInfo).hidden = true;
+ ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled');
+ ankiErrorMessageNode.classList.remove('danger-text');
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsNode).textContent = '';
this._ankiError = null;
}
+ /**
+ * @param {Error} error
+ */
_showAnkiError(error) {
+ const ankiErrorMessageNode = /** @type {HTMLElement} */ (this._ankiErrorMessageNode);
this._ankiError = error;
let errorString = typeof error === 'object' && error !== null ? error.message : null;
if (!errorString) { errorString = `${error}`; }
if (!/[.!?]$/.test(errorString)) { errorString += '.'; }
- this._ankiErrorMessageNode.textContent = errorString;
- this._ankiErrorMessageNode.classList.add('danger-text');
+ ankiErrorMessageNode.textContent = errorString;
+ ankiErrorMessageNode.classList.add('danger-text');
- const data = error.data;
+ const data = error instanceof ExtensionError ? error.data : void 0;
let details = '';
if (typeof data !== 'undefined') {
details += `${JSON.stringify(data, null, 4)}\n\n`;
}
details += `${error.stack}`.trimRight();
- this._ankiErrorMessageDetailsNode.textContent = details;
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsNode).textContent = details;
if (this._ankiErrorContainer !== null) {
this._ankiErrorContainer.hidden = false;
}
- this._ankiErrorMessageDetailsContainer.hidden = true;
- this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0);
- this._ankiErrorMessageDetailsToggle.hidden = false;
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsContainer).hidden = true;
+ /** @type {HTMLElement} */ (this._ankiErrorInvalidResponseInfo).hidden = (errorString.indexOf('Invalid response') < 0);
+ /** @type {HTMLElement} */ (this._ankiErrorMessageDetailsToggle).hidden = false;
}
+ /**
+ * @param {string[]} array
+ */
_sortStringArray(array) {
const stringComparer = this._stringComparer;
array.sort((a, b) => stringComparer.compare(a, b));
}
+ /**
+ * @param {import('settings').AnkiNoteGuiMode} mode
+ */
async _testAnkiNoteViewerSafe(mode) {
this._setAnkiNoteViewerStatus(false, null);
try {
await this._testAnkiNoteViewer(mode);
} catch (e) {
- this._setAnkiNoteViewerStatus(true, e);
+ this._setAnkiNoteViewerStatus(true, e instanceof Error ? e : new Error(`${e}`));
return;
}
this._setAnkiNoteViewerStatus(true, null);
}
+ /**
+ * @param {import('settings').AnkiNoteGuiMode} mode
+ */
async _testAnkiNoteViewer(mode) {
const queries = [
'"よむ" deck:current',
@@ -408,8 +528,12 @@ export class AnkiController {
await yomitan.api.noteView(noteId, mode, false);
}
+ /**
+ * @param {boolean} visible
+ * @param {?Error} error
+ */
_setAnkiNoteViewerStatus(visible, error) {
- const node = document.querySelector('#test-anki-note-viewer-results');
+ const node = /** @type {HTMLElement} */ (document.querySelector('#test-anki-note-viewer-results'));
if (visible) {
const success = (error === null);
node.textContent = success ? 'Success!' : error.message;
@@ -420,26 +544,61 @@ export class AnkiController {
}
node.hidden = !visible;
}
+
+ /**
+ * @param {string} value
+ * @returns {?import('settings').AnkiNoteGuiMode}
+ */
+ _normalizeAnkiNoteGuiMode(value) {
+ switch (value) {
+ case 'browse':
+ case 'edit':
+ return value;
+ default:
+ return null;
+ }
+ }
}
class AnkiCardController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {AnkiController} ankiController
+ * @param {HTMLElement} node
+ */
constructor(settingsController, ankiController, node) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {AnkiController} */
this._ankiController = ankiController;
+ /** @type {HTMLElement} */
this._node = node;
- this._cardType = node.dataset.ankiCardType;
+ const {ankiCardType} = node.dataset;
+ /** @type {string} */
+ this._cardType = typeof ankiCardType === 'string' ? ankiCardType : 'terms';
+ /** @type {string|undefined} */
this._cardMenu = node.dataset.ankiCardMenu;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {EventListenerCollection} */
this._fieldEventListeners = new EventListenerCollection();
- this._fields = null;
+ /** @type {import('settings').AnkiNoteFields} */
+ this._fields = {};
+ /** @type {?string} */
this._modelChangingTo = null;
+ /** @type {?Element} */
this._ankiCardFieldsContainer = null;
+ /** @type {boolean} */
this._cleaned = false;
+ /** @type {import('anki-controller').FieldEntry[]} */
this._fieldEntries = [];
+ /** @type {AnkiCardSelectController} */
this._deckController = new AnkiCardSelectController();
+ /** @type {AnkiCardSelectController} */
this._modelController = new AnkiCardSelectController();
}
+ /** */
async prepare() {
const options = await this._settingsController.getOptions();
const ankiOptions = options.anki;
@@ -448,8 +607,8 @@ class AnkiCardController {
const cardOptions = this._getCardOptions(ankiOptions, this._cardType);
if (cardOptions === null) { return; }
const {deck, model, fields} = cardOptions;
- this._deckController.prepare(this._node.querySelector('.anki-card-deck'), deck);
- this._modelController.prepare(this._node.querySelector('.anki-card-model'), model);
+ this._deckController.prepare(/** @type {HTMLSelectElement} */ (this._node.querySelector('.anki-card-deck')), deck);
+ this._modelController.prepare(/** @type {HTMLSelectElement} */ (this._node.querySelector('.anki-card-model')), model);
this._fields = fields;
this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields');
@@ -463,12 +622,14 @@ class AnkiCardController {
await this.updateAnkiState();
}
+ /** */
cleanup() {
this._cleaned = true;
this._fieldEntries = [];
this._eventListeners.removeAllEventListeners();
}
+ /** */
async updateAnkiState() {
if (this._fields === null) { return; }
const {deckNames, modelNames} = await this._ankiController.getAnkiData();
@@ -477,41 +638,70 @@ class AnkiCardController {
this._modelController.setOptionValues(modelNames);
}
+ /**
+ * @returns {boolean}
+ */
isStale() {
return (this._cardType !== this._node.dataset.ankiCardType);
}
// Private
+ /**
+ * @param {Event} e
+ */
_onCardDeckChange(e) {
- this._setDeck(e.currentTarget.value);
+ const node = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ this._setDeck(node.value);
}
+ /**
+ * @param {Event} e
+ */
_onCardModelChange(e) {
- this._setModel(e.currentTarget.value);
+ const node = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ this._setModel(node.value);
}
+ /**
+ * @param {number} index
+ * @param {Event} e
+ */
_onFieldChange(index, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
this._validateFieldPermissions(node, index, true);
this._validateField(node, index);
}
+ /**
+ * @param {number} index
+ * @param {Event} e
+ */
_onFieldInput(index, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
this._validateField(node, index);
}
+ /**
+ * @param {number} index
+ * @param {import('dom-data-binder').SettingChangedEvent} e
+ */
_onFieldSettingChanged(index, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
this._validateFieldPermissions(node, index, false);
}
- _onFieldMenuOpen({currentTarget: button, detail: {menu}}) {
- let {index, fieldName} = button.dataset;
- index = Number.parseInt(index, 10);
-
- const defaultValue = this._getDefaultFieldValue(fieldName, index, this._cardType, null);
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} event
+ */
+ _onFieldMenuOpen(event) {
+ const button = /** @type {HTMLElement} */ (event.currentTarget);
+ const {menu} = event.detail;
+ const {index, fieldName} = button.dataset;
+ const indexNumber = typeof index === 'string' ? Number.parseInt(index, 10) : 0;
+ if (typeof fieldName !== 'string') { return; }
+
+ const defaultValue = this._getDefaultFieldValue(fieldName, indexNumber, this._cardType, null);
if (defaultValue === '') { return; }
const match = /^\{([\w\W]+)\}$/.exec(defaultValue);
@@ -524,14 +714,28 @@ class AnkiCardController {
item.classList.add('popup-menu-item-bold');
}
- _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} event
+ */
+ _onFieldMenuClose(event) {
+ const button = /** @type {HTMLElement} */ (event.currentTarget);
+ const {action, item} = event.detail;
switch (action) {
case 'setFieldMarker':
- this._setFieldMarker(button, item.dataset.marker);
+ if (item !== null) {
+ const {marker} = item.dataset;
+ if (typeof marker === 'string') {
+ this._setFieldMarker(button, marker);
+ }
+ }
break;
}
}
+ /**
+ * @param {HTMLInputElement} node
+ * @param {number} index
+ */
_validateField(node, index) {
let valid = (node.dataset.hasPermissions !== 'false');
if (valid && index === 0 && !AnkiUtil.stringContainsAnyFieldMarker(node.value)) {
@@ -540,12 +744,23 @@ class AnkiCardController {
node.dataset.invalid = `${!valid}`;
}
+ /**
+ * @param {Element} element
+ * @param {string} marker
+ */
_setFieldMarker(element, marker) {
- const input = element.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value');
+ const container = element.closest('.anki-card-field-value-container');
+ if (container === null) { return; }
+ const input = /** @type {HTMLInputElement} */ (container.querySelector('.anki-card-field-value'));
input.value = `{${marker}}`;
input.dispatchEvent(new Event('change'));
}
+ /**
+ * @param {import('settings').AnkiOptions} ankiOptions
+ * @param {string} cardType
+ * @returns {?import('settings').AnkiNoteOptions}
+ */
_getCardOptions(ankiOptions, cardType) {
switch (cardType) {
case 'terms': return ankiOptions.terms;
@@ -554,6 +769,7 @@ class AnkiCardController {
}
}
+ /** */
_setupFields() {
this._fieldEventListeners.removeAllEventListeners();
@@ -563,15 +779,15 @@ class AnkiCardController {
for (const [fieldName, fieldValue] of Object.entries(this._fields)) {
const content = this._settingsController.instantiateTemplateFragment('anki-card-field');
- const fieldNameContainerNode = content.querySelector('.anki-card-field-name-container');
+ const fieldNameContainerNode = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-name-container'));
fieldNameContainerNode.dataset.index = `${index}`;
- const fieldNameNode = content.querySelector('.anki-card-field-name');
+ const fieldNameNode = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-name'));
fieldNameNode.textContent = fieldName;
- const valueContainer = content.querySelector('.anki-card-field-value-container');
+ const valueContainer = /** @type {HTMLElement} */ (content.querySelector('.anki-card-field-value-container'));
valueContainer.dataset.index = `${index}`;
- const inputField = content.querySelector('.anki-card-field-value');
+ const inputField = /** @type {HTMLInputElement} */ (content.querySelector('.anki-card-field-value'));
inputField.value = fieldValue;
inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]);
this._validateFieldPermissions(inputField, index, false);
@@ -581,7 +797,7 @@ class AnkiCardController {
this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false);
this._validateField(inputField, index);
- const menuButton = content.querySelector('.anki-card-field-value-menu-button');
+ const menuButton = /** @type {?HTMLElement} */ (content.querySelector('.anki-card-field-value-menu-button'));
if (menuButton !== null) {
if (typeof this._cardMenu !== 'undefined') {
menuButton.dataset.menu = this._cardMenu;
@@ -602,15 +818,18 @@ class AnkiCardController {
const ELEMENT_NODE = Node.ELEMENT_NODE;
const container = this._ankiCardFieldsContainer;
- for (const node of [...container.childNodes]) {
- if (node.nodeType === ELEMENT_NODE && node.dataset.persistent === 'true') { continue; }
- container.removeChild(node);
+ if (container !== null) {
+ for (const node of [...container.childNodes]) {
+ if (node.nodeType === ELEMENT_NODE && node instanceof HTMLElement && node.dataset.persistent === 'true') { continue; }
+ container.removeChild(node);
+ }
+ container.appendChild(totalFragment);
}
- container.appendChild(totalFragment);
this._validateFields();
}
+ /** */
async _validateFields() {
const token = {};
this._validateFieldsToken = token;
@@ -633,6 +852,9 @@ class AnkiCardController {
}
}
+ /**
+ * @param {string} value
+ */
async _setDeck(value) {
if (this._deckController.value === value) { return; }
this._deckController.value = value;
@@ -644,6 +866,9 @@ class AnkiCardController {
}]);
}
+ /**
+ * @param {string} value
+ */
async _setModel(value) {
const select = this._modelController.select;
if (this._modelChangingTo !== null) {
@@ -671,12 +896,14 @@ class AnkiCardController {
const cardOptions = this._getCardOptions(options.anki, cardType);
const oldFields = cardOptions !== null ? cardOptions.fields : null;
+ /** @type {import('settings').AnkiNoteFields} */
const fields = {};
for (let i = 0, ii = fieldNames.length; i < ii; ++i) {
const fieldName = fieldNames[i];
fields[fieldName] = this._getDefaultFieldValue(fieldName, i, cardType, oldFields);
}
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [
{
action: 'set',
@@ -698,6 +925,9 @@ class AnkiCardController {
this._setupFields();
}
+ /**
+ * @param {string[]} permissions
+ */
async _requestPermissions(permissions) {
try {
await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true);
@@ -706,6 +936,11 @@ class AnkiCardController {
}
}
+ /**
+ * @param {HTMLInputElement} node
+ * @param {number} index
+ * @param {boolean} request
+ */
async _validateFieldPermissions(node, index, request) {
const fieldValue = node.value;
const permissions = this._ankiController.getRequiredPermissions(fieldValue);
@@ -725,16 +960,19 @@ class AnkiCardController {
this._validateField(node, index);
}
+ /**
+ * @param {import('settings-controller').PermissionsChangedEvent} details
+ */
_onPermissionsChanged({permissions: {permissions}}) {
const permissionsSet = new Set(permissions);
for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) {
const {inputField} = this._fieldEntries[i];
- let {requiredPermission} = inputField.dataset;
+ const {requiredPermission} = inputField.dataset;
if (typeof requiredPermission !== 'string') { continue; }
- requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' '));
+ const requiredPermissionArray = (requiredPermission.length === 0 ? [] : requiredPermission.split(' '));
let hasPermissions = true;
- for (const permission of requiredPermission) {
+ for (const permission of requiredPermissionArray) {
if (!permissionsSet.has(permission)) {
hasPermissions = false;
break;
@@ -746,6 +984,13 @@ class AnkiCardController {
}
}
+ /**
+ * @param {string} fieldName
+ * @param {number} index
+ * @param {string} cardType
+ * @param {?import('settings').AnkiNoteFields} oldFields
+ * @returns {string}
+ */
_getDefaultFieldValue(fieldName, index, cardType, oldFields) {
if (
typeof oldFields === 'object' &&
@@ -783,9 +1028,9 @@ class AnkiCardController {
pattern += name.replace(hyphenPattern, '[-_ ]*');
}
pattern += ')$';
- pattern = new RegExp(pattern, 'i');
+ const patternRegExp = new RegExp(pattern, 'i');
- if (pattern.test(fieldName)) {
+ if (patternRegExp.test(fieldName)) {
return `{${marker}}`;
}
}
@@ -796,14 +1041,21 @@ class AnkiCardController {
class AnkiCardSelectController {
constructor() {
+ /** @type {?string} */
this._value = null;
+ /** @type {?HTMLSelectElement} */
this._select = null;
- this._optionValues = null;
+ /** @type {string[]} */
+ this._optionValues = [];
+ /** @type {boolean} */
this._hasExtraOption = false;
+ /** @type {boolean} */
this._selectNeedsUpdate = false;
}
+ /** @type {string} */
get value() {
+ if (this._value === null) { throw new Error('Invalid value'); }
return this._value;
}
@@ -812,16 +1064,25 @@ class AnkiCardSelectController {
this._updateSelect();
}
+ /** @type {HTMLSelectElement} */
get select() {
+ if (this._select === null) { throw new Error('Invalid value'); }
return this._select;
}
+ /**
+ * @param {HTMLSelectElement} select
+ * @param {string} value
+ */
prepare(select, value) {
this._select = select;
this._value = value;
this._updateSelect();
}
+ /**
+ * @param {string[]} optionValues
+ */
setOptionValues(optionValues) {
this._optionValues = optionValues;
this._selectNeedsUpdate = true;
@@ -830,8 +1091,11 @@ class AnkiCardSelectController {
// Private
+ /** */
_updateSelect() {
+ const select = this._select;
const value = this._value;
+ if (select === null || value === null) { return; }
let optionValues = this._optionValues;
const hasOptionValues = Array.isArray(optionValues) && optionValues.length > 0;
@@ -844,7 +1108,6 @@ class AnkiCardSelectController {
optionValues = [...optionValues, value];
}
- const select = this._select;
if (this._selectNeedsUpdate || hasExtraOption !== this._hasExtraOption) {
this._setSelectOptions(select, optionValues);
select.value = value;
@@ -859,6 +1122,10 @@ class AnkiCardSelectController {
}
}
+ /**
+ * @param {HTMLSelectElement} select
+ * @param {string[]} optionValues
+ */
_setSelectOptions(select, optionValues) {
const fragment = document.createDocumentFragment();
for (const optionValue of optionValues) {
diff --git a/ext/js/pages/settings/anki-templates-controller.js b/ext/js/pages/settings/anki-templates-controller.js
index ac8a0205..89848ef3 100644
--- a/ext/js/pages/settings/anki-templates-controller.js
+++ b/ext/js/pages/settings/anki-templates-controller.js
@@ -16,39 +16,57 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../../core.js';
+import {ExtensionError} from '../../core/extension-error.js';
import {AnkiNoteBuilder} from '../../data/anki-note-builder.js';
import {JapaneseUtil} from '../../language/sandbox/japanese-util.js';
import {yomitan} from '../../yomitan.js';
export class AnkiTemplatesController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ * @param {import('./anki-controller.js').AnkiController} ankiController
+ */
constructor(settingsController, modalController, ankiController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {import('./anki-controller.js').AnkiController} */
this._ankiController = ankiController;
+ /** @type {?import('dictionary').TermDictionaryEntry} */
this._cachedDictionaryEntryValue = null;
+ /** @type {?string} */
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 {?import('./modal.js').Modal} */
this._fieldTemplateResetModal = null;
+ /** @type {AnkiNoteBuilder} */
this._ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil: new JapaneseUtil(null)});
}
+ /** */
async prepare() {
this._defaultFieldTemplates = await yomitan.api.getDefaultAnkiFieldTemplates();
- this._fieldTemplatesTextarea = document.querySelector('#anki-card-templates-textarea');
- this._compileResultInfo = document.querySelector('#anki-card-templates-compile-result');
- this._renderFieldInput = document.querySelector('#anki-card-templates-test-field-input');
- this._renderTextInput = document.querySelector('#anki-card-templates-test-text-input');
- this._renderResult = document.querySelector('#anki-card-templates-render-result');
- const menuButton = document.querySelector('#anki-card-templates-test-field-menu-button');
- const testRenderButton = document.querySelector('#anki-card-templates-test-render-button');
- const resetButton = document.querySelector('#anki-card-templates-reset-button');
- const resetConfirmButton = document.querySelector('#anki-card-templates-reset-button-confirm');
+ 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'));
this._fieldTemplateResetModal = this._modalController.getModal('anki-card-templates-reset');
this._fieldTemplatesTextarea.addEventListener('change', this._onChanged.bind(this), false);
@@ -56,44 +74,71 @@ export class AnkiTemplatesController {
resetButton.addEventListener('click', this._onReset.bind(this), false);
resetConfirmButton.addEventListener('click', this._onResetConfirm.bind(this), false);
if (menuButton !== null) {
- menuButton.addEventListener('menuClose', this._onFieldMenuClose.bind(this), false);
+ menuButton.addEventListener(
+ /** @type {string} */ ('menuClose'),
+ /** @type {EventListener} */ (this._onFieldMenuClose.bind(this)),
+ false
+ );
}
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
let templates = options.anki.fieldTemplates;
- if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
- this._fieldTemplatesTextarea.value = templates;
+ if (typeof templates !== 'string') {
+ templates = this._defaultFieldTemplates;
+ if (typeof templates !== 'string') { templates = ''; }
+ }
+ /** @type {HTMLTextAreaElement} */ (this._fieldTemplatesTextarea).value = templates;
this._onValidateCompile();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onReset(e) {
e.preventDefault();
- this._fieldTemplateResetModal.setVisible(true);
+ if (this._fieldTemplateResetModal !== null) {
+ this._fieldTemplateResetModal.setVisible(true);
+ }
}
+ /**
+ * @param {MouseEvent} e
+ */
_onResetConfirm(e) {
e.preventDefault();
- this._fieldTemplateResetModal.setVisible(false);
+ if (this._fieldTemplateResetModal !== null) {
+ this._fieldTemplateResetModal.setVisible(false);
+ }
const value = this._defaultFieldTemplates;
- this._fieldTemplatesTextarea.value = value;
- this._fieldTemplatesTextarea.dispatchEvent(new Event('change'));
+ const textarea = /** @type {HTMLTextAreaElement} */ (this._fieldTemplatesTextarea);
+ textarea.value = typeof value === 'string' ? value : '';
+ textarea.dispatchEvent(new Event('change'));
}
+ /**
+ * @param {Event} e
+ */
async _onChanged(e) {
// Get value
- let templates = e.currentTarget.value;
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ /** @type {?string} */
+ let templates = element.value;
if (templates === this._defaultFieldTemplates) {
// Default
templates = null;
@@ -106,34 +151,55 @@ export class AnkiTemplatesController {
this._onValidateCompile();
}
+ /** */
_onValidateCompile() {
+ if (this._compileResultInfo === null) { return; }
this._validate(this._compileResultInfo, '{expression}', 'term-kanji', false, true);
}
+ /**
+ * @param {Event} e
+ */
_onRender(e) {
e.preventDefault();
- const field = this._renderFieldInput.value;
- const infoNode = this._renderResult;
+ const field = /** @type {HTMLInputElement} */ (this._renderFieldInput).value;
+ const infoNode = /** @type {HTMLElement} */ (this._renderResult);
infoNode.hidden = true;
this._cachedDictionaryEntryText = null;
this._validate(infoNode, field, 'term-kanji', true, false);
}
- _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} event
+ */
+ _onFieldMenuClose({detail: {action, item}}) {
switch (action) {
case 'setFieldMarker':
- this._setFieldMarker(button, item.dataset.marker);
+ {
+ const {marker} = /** @type {HTMLElement} */ (item).dataset;
+ if (typeof marker === 'string') {
+ this._setFieldMarker(marker);
+ }
+ }
break;
}
}
- _setFieldMarker(element, marker) {
- const input = this._renderFieldInput;
+ /**
+ * @param {string} marker
+ */
+ _setFieldMarker(marker) {
+ const input = /** @type {HTMLInputElement} */ (this._renderFieldInput);
input.value = `{${marker}}`;
input.dispatchEvent(new Event('change'));
}
+ /**
+ * @param {string} text
+ * @param {import('settings').OptionsContext} optionsContext
+ * @returns {Promise<?{dictionaryEntry: import('dictionary').TermDictionaryEntry, text: string}>}
+ */
async _getDictionaryEntry(text, optionsContext) {
if (this._cachedDictionaryEntryText !== text) {
const {dictionaryEntries} = await yomitan.api.termsFind(text, {}, optionsContext);
@@ -143,19 +209,28 @@ export class AnkiTemplatesController {
this._cachedDictionaryEntryText = text;
}
return {
- dictionaryEntry: this._cachedDictionaryEntryValue,
+ dictionaryEntry: /** @type {import('dictionary').TermDictionaryEntry} */ (this._cachedDictionaryEntryValue),
text: this._cachedDictionaryEntryText
};
}
+ /**
+ * @param {HTMLElement} infoNode
+ * @param {string} field
+ * @param {import('anki-templates-internal').CreateModeNoTest} mode
+ * @param {boolean} showSuccessResult
+ * @param {boolean} invalidateInput
+ */
async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
+ /** @type {Error[]} */
const allErrors = [];
- const text = this._renderTextInput.value || '';
+ const text = /** @type {HTMLInputElement} */ (this._renderTextInput).value;
let result = `No definition found for ${text}`;
try {
const optionsContext = this._settingsController.getOptionsContext();
- const {dictionaryEntry, text: sentenceText} = await this._getDictionaryEntry(text, optionsContext);
- if (dictionaryEntry !== null) {
+ const data = await this._getDictionaryEntry(text, optionsContext);
+ if (data !== null) {
+ const {dictionaryEntry, text: sentenceText} = data;
const options = await this._settingsController.getOptions();
const context = {
url: window.location.href,
@@ -170,7 +245,7 @@ export class AnkiTemplatesController {
let template = options.anki.fieldTemplates;
if (typeof template !== 'string') { template = this._defaultFieldTemplates; }
const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = options;
- const {note, errors} = await this._ankiNoteBuilder.createNote({
+ const {note, errors} = await this._ankiNoteBuilder.createNote(/** @type {import('anki-note-builder').CreateNoteDetails} */ ({
dictionaryEntry,
mode,
context,
@@ -183,28 +258,29 @@ export class AnkiTemplatesController {
resultOutputMode,
glossaryLayoutMode,
compactTags
- });
+ }));
result = note.fields.field;
allErrors.push(...errors);
}
} catch (e) {
- allErrors.push(e);
+ allErrors.push(e instanceof Error ? e : new Error(`${e}`));
}
+ /**
+ * @param {Error} e
+ * @returns {string}
+ */
const errorToMessageString = (e) => {
- if (isObject(e)) {
- let v = e.data;
- if (isObject(v)) {
- v = v.error;
- if (isObject(v)) {
- e = v;
+ if (e instanceof ExtensionError) {
+ const v = e.data;
+ if (typeof v === 'object' && v !== null) {
+ const v2 = /** @type {import('core').UnknownObject} */ (v).error;
+ if (v2 instanceof Error) {
+ return v2.message;
}
}
-
- v = e.message;
- if (typeof v === 'string') { return v; }
}
- return `${e}`;
+ return e.message;
};
const hasError = allErrors.length > 0;
@@ -212,7 +288,7 @@ export class AnkiTemplatesController {
infoNode.textContent = hasError ? allErrors.map(errorToMessageString).join('\n') : (showSuccessResult ? result : '');
infoNode.classList.toggle('text-danger', hasError);
if (invalidateInput) {
- this._fieldTemplatesTextarea.dataset.invalid = `${hasError}`;
+ /** @type {HTMLTextAreaElement} */ (this._fieldTemplatesTextarea).dataset.invalid = `${hasError}`;
}
}
}
diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js
index fb54ee6b..0a3f9454 100644
--- a/ext/js/pages/settings/audio-controller.js
+++ b/ext/js/pages/settings/audio-controller.js
@@ -19,48 +19,71 @@
import {EventDispatcher, EventListenerCollection} from '../../core.js';
import {AudioSystem} from '../../media/audio-system.js';
+/**
+ * @augments EventDispatcher<import('audio-controller').EventType>
+ */
export class AudioController extends EventDispatcher {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ */
constructor(settingsController, modalController) {
super();
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {AudioSystem} */
this._audioSystem = new AudioSystem();
+ /** @type {?HTMLElement} */
this._audioSourceContainer = null;
+ /** @type {?HTMLButtonElement} */
this._audioSourceAddButton = null;
+ /** @type {AudioSourceEntry[]} */
this._audioSourceEntries = [];
+ /** @type {?HTMLInputElement} */
this._voiceTestTextInput = null;
+ /** @type {import('audio-controller').VoiceInfo[]} */
this._voices = [];
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._settingsController;
}
+ /** @type {import('./modal-controller.js').ModalController} */
get modalController() {
return this._modalController;
}
+ /** */
async prepare() {
this._audioSystem.prepare();
- this._voiceTestTextInput = document.querySelector('#text-to-speech-voice-test-text');
- this._audioSourceContainer = document.querySelector('#audio-source-list');
- this._audioSourceAddButton = document.querySelector('#audio-source-add');
+ 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'));
this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false);
- this._audioSystem.on('voiceschanged', this._updateTextToSpeechVoices.bind(this), false);
+ this._audioSystem.on('voiceschanged', this._updateTextToSpeechVoices.bind(this));
this._updateTextToSpeechVoices();
- document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._onTestTextToSpeech.bind(this), false);
+ testButton.addEventListener('click', this._onTestTextToSpeech.bind(this), false);
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /**
+ * @param {AudioSourceEntry} entry
+ */
async removeSource(entry) {
const {index} = entry;
this._audioSourceEntries.splice(index, 1);
@@ -78,16 +101,25 @@ export class AudioController extends EventDispatcher {
}]);
}
+ /**
+ * @returns {import('audio-controller').VoiceInfo[]}
+ */
getVoices() {
return this._voices;
}
+ /**
+ * @param {string} voice
+ */
setTestVoice(voice) {
- this._voiceTestTextInput.dataset.voice = voice;
+ /** @type {HTMLInputElement} */ (this._voiceTestTextInput).dataset.voice = voice;
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
for (const entry of this._audioSourceEntries) {
entry.cleanup();
@@ -100,15 +132,18 @@ export class AudioController extends EventDispatcher {
}
}
+ /** */
_onAddAudioSource() {
this._addAudioSource();
}
+ /** */
_onTestTextToSpeech() {
try {
- const text = this._voiceTestTextInput.value || '';
- const voiceUri = this._voiceTestTextInput.dataset.voice;
- const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri);
+ const input = /** @type {HTMLInputElement} */ (this._voiceTestTextInput);
+ const text = input.value || '';
+ const voiceUri = input.dataset.voice;
+ const audio = this._audioSystem.createTextToSpeechAudio(text, typeof voiceUri === 'string' ? voiceUri : '');
audio.volume = 1.0;
audio.play();
} catch (e) {
@@ -116,6 +151,7 @@ export class AudioController extends EventDispatcher {
}
}
+ /** */
_updateTextToSpeechVoices() {
const voices = (
typeof speechSynthesis !== 'undefined' ?
@@ -131,6 +167,11 @@ export class AudioController extends EventDispatcher {
this.trigger('voicesUpdated');
}
+ /**
+ * @param {import('audio-controller').VoiceInfo} a
+ * @param {import('audio-controller').VoiceInfo} b
+ * @returns {number}
+ */
_textToSpeechVoiceCompare(a, b) {
if (a.isJapanese) {
if (!b.isJapanese) { return -1; }
@@ -147,6 +188,10 @@ export class AudioController extends EventDispatcher {
return a.index - b.index;
}
+ /**
+ * @param {string} languageTag
+ * @returns {boolean}
+ */
_languageTagIsJapanese(languageTag) {
return (
languageTag.startsWith('ja_') ||
@@ -155,15 +200,23 @@ export class AudioController extends EventDispatcher {
);
}
+ /**
+ * @param {number} index
+ * @param {import('settings').AudioSourceOptions} source
+ */
_createAudioSourceEntry(index, source) {
- const node = this._settingsController.instantiateTemplate('audio-source');
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('audio-source'));
const entry = new AudioSourceEntry(this, index, source, node);
this._audioSourceEntries.push(entry);
- this._audioSourceContainer.appendChild(node);
+ /** @type {HTMLElement} */ (this._audioSourceContainer).appendChild(node);
entry.prepare();
}
+ /**
+ * @returns {import('settings').AudioSourceType}
+ */
_getUnusedAudioSourceType() {
+ /** @type {import('settings').AudioSourceType[]} */
const typesAvailable = [
'jpod101',
'jpod101-alternate',
@@ -178,8 +231,10 @@ export class AudioController extends EventDispatcher {
return typesAvailable[0];
}
+ /** */
async _addAudioSource() {
const type = this._getUnusedAudioSourceType();
+ /** @type {import('settings').AudioSourceOptions} */
const source = {type, url: '', voice: ''};
const index = this._audioSourceEntries.length;
this._createAudioSourceEntry(index, source);
@@ -194,19 +249,36 @@ export class AudioController extends EventDispatcher {
}
class AudioSourceEntry {
+ /**
+ * @param {AudioController} parent
+ * @param {number} index
+ * @param {import('settings').AudioSourceOptions} source
+ * @param {HTMLElement} node
+ */
constructor(parent, index, source, node) {
+ /** @type {AudioController} */
this._parent = parent;
+ /** @type {number} */
this._index = index;
+ /** @type {import('settings').AudioSourceType} */
this._type = source.type;
+ /** @type {string} */
this._url = source.url;
+ /** @type {string} */
this._voice = source.voice;
+ /** @type {HTMLElement} */
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 {number} */
get index() {
return this._index;
}
@@ -215,17 +287,19 @@ class AudioSourceEntry {
this._index = value;
}
+ /** @type {import('settings').AudioSourceType} */
get type() {
return this._type;
}
+ /** */
prepare() {
this._updateTypeParameter();
- const menuButton = this._node.querySelector('.audio-source-menu-button');
- this._typeSelect = this._node.querySelector('.audio-source-type-select');
- this._urlInput = this._node.querySelector('.audio-source-parameter-container[data-field=url] .audio-source-parameter');
- this._voiceSelect = this._node.querySelector('.audio-source-parameter-container[data-field=voice] .audio-source-parameter');
+ 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'));
this._typeSelect.value = this._type;
this._urlInput.value = this._url;
@@ -239,6 +313,7 @@ class AudioSourceEntry {
this._onVoicesUpdated();
}
+ /** */
cleanup() {
if (this._node.parentNode !== null) {
this._node.parentNode.removeChild(this._node);
@@ -248,7 +323,9 @@ class AudioSourceEntry {
// Private
+ /** */
_onVoicesUpdated() {
+ if (this._voiceSelect === null) { return; }
const voices = this._parent.getVoices();
const fragment = document.createDocumentFragment();
@@ -270,18 +347,35 @@ class AudioSourceEntry {
this._voiceSelect.value = this._voice;
}
+ /**
+ * @param {Event} e
+ */
_onTypeSelectChange(e) {
- this._setType(e.currentTarget.value);
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const value = this._normalizeAudioSourceType(element.value);
+ if (value === null) { return; }
+ this._setType(value);
}
+ /**
+ * @param {Event} e
+ */
_onUrlInputChange(e) {
- this._setUrl(e.currentTarget.value);
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ this._setUrl(element.value);
}
+ /**
+ * @param {Event} e
+ */
_onVoiceSelectChange(e) {
- this._setVoice(e.currentTarget.value);
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ this._setVoice(element.value);
}
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const {menu} = e.detail;
@@ -295,9 +389,15 @@ class AudioSourceEntry {
break;
}
- menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]').hidden = !hasHelp;
+ const helpNode = /** @type {?HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]'));
+ if (helpNode !== null) {
+ helpNode.hidden = !hasHelp;
+ }
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'help':
@@ -309,22 +409,32 @@ class AudioSourceEntry {
}
}
+ /**
+ * @param {import('settings').AudioSourceType} value
+ */
async _setType(value) {
this._type = value;
this._updateTypeParameter();
await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].type`, value);
}
+ /**
+ * @param {string} value
+ */
async _setUrl(value) {
this._url = value;
await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].url`, value);
}
+ /**
+ * @param {string} value
+ */
async _setVoice(value) {
this._voice = value;
await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].voice`, value);
}
+ /** */
_updateTypeParameter() {
let field = null;
switch (this._type) {
@@ -337,11 +447,14 @@ class AudioSourceEntry {
field = 'voice';
break;
}
- for (const node of this._node.querySelectorAll('.audio-source-parameter-container')) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._node.querySelectorAll('.audio-source-parameter-container'))) {
node.hidden = (field === null || node.dataset.field !== field);
}
}
+ /**
+ * @param {import('settings').AudioSourceType} type
+ */
_showHelp(type) {
switch (type) {
case 'custom':
@@ -358,7 +471,31 @@ class AudioSourceEntry {
}
}
+ /**
+ * @param {string} name
+ */
_showModal(name) {
- this._parent.modalController.getModal(name).setVisible(true);
+ const modal = this._parent.modalController.getModal(name);
+ if (modal === null) { return; }
+ modal.setVisible(true);
+ }
+
+ /**
+ * @param {string} value
+ * @returns {?import('settings').AudioSourceType}
+ */
+ _normalizeAudioSourceType(value) {
+ switch (value) {
+ case 'jpod101':
+ case 'jpod101-alternate':
+ case 'jisho':
+ case 'text-to-speech':
+ case 'text-to-speech-reading':
+ case 'custom':
+ case 'custom-json':
+ return value;
+ default:
+ return null;
+ }
}
}
diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js
index 2863c505..bf44bb90 100644
--- a/ext/js/pages/settings/backup-controller.js
+++ b/ext/js/pages/settings/backup-controller.js
@@ -24,18 +24,37 @@ import {yomitan} from '../../yomitan.js';
import {DictionaryController} from './dictionary-controller.js';
export class BackupController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {?import('./modal-controller.js').ModalController} modalController
+ */
constructor(settingsController, modalController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {?import('core').TokenObject} */
this._settingsExportToken = null;
+ /** @type {?() => void} */
this._settingsExportRevoke = null;
+ /** @type {number} */
this._currentVersion = 0;
+ /** @type {?import('./modal.js').Modal} */
this._settingsResetModal = null;
+ /** @type {?import('./modal.js').Modal} */
this._settingsImportErrorModal = null;
+ /** @type {?import('./modal.js').Modal} */
this._settingsImportWarningModal = null;
+ /** @type {?OptionsUtil} */
this._optionsUtil = null;
+ /**
+ *
+ */
this._dictionariesDatabaseName = 'dict';
+ /**
+ *
+ */
this._settingsExportDatabaseToken = null;
try {
@@ -45,6 +64,7 @@ export class BackupController {
}
}
+ /** */
async prepare() {
if (this._optionsUtil !== null) {
await this._optionsUtil.prepare();
@@ -69,13 +89,27 @@ export class BackupController {
// Private
- _addNodeEventListener(selector, ...args) {
+ /**
+ * @param {string} selector
+ * @param {string} eventName
+ * @param {(event: Event) => void} callback
+ * @param {boolean} capture
+ */
+ _addNodeEventListener(selector, eventName, callback, capture) {
const node = document.querySelector(selector);
if (node === null) { return; }
- node.addEventListener(...args);
+ node.addEventListener(eventName, callback, capture);
}
+ /**
+ * @param {Date} date
+ * @param {string} dateSeparator
+ * @param {string} dateTimeSeparator
+ * @param {string} timeSeparator
+ * @param {number} resolution
+ * @returns {string}
+ */
_getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) {
const values = [
date.getUTCFullYear().toString(),
@@ -93,6 +127,10 @@ export class BackupController {
return values.slice(0, resolution * 2 - 1).join('');
}
+ /**
+ * @param {Date} date
+ * @returns {Promise<import('backup-controller').BackupData>}
+ */
async _getSettingsExportData(date) {
const optionsFull = await this._settingsController.getOptionsFull();
const environment = await yomitan.api.getEnvironmentInfo();
@@ -120,11 +158,19 @@ export class BackupController {
return data;
}
+ /**
+ * @param {Blob} blob
+ * @param {string} fileName
+ */
_saveBlob(blob, fileName) {
- if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
- if (navigator.msSaveBlob(blob)) {
- return;
- }
+ if (
+ typeof navigator === 'object' && navigator !== null &&
+ // @ts-expect-error - call for legacy Edge
+ typeof navigator.msSaveBlob === 'function' &&
+ // @ts-expect-error - call for legacy Edge
+ navigator.msSaveBlob(blob)
+ ) {
+ return;
}
const blobUrl = URL.createObjectURL(blob);
@@ -146,6 +192,7 @@ export class BackupController {
setTimeout(revoke, 60000);
}
+ /** */
async _onSettingsExportClick() {
if (this._settingsExportRevoke !== null) {
this._settingsExportRevoke();
@@ -154,6 +201,7 @@ export class BackupController {
const date = new Date(Date.now());
+ /** @type {?import('core').TokenObject} */
const token = {};
this._settingsExportToken = token;
const data = await this._getSettingsExportData(date);
@@ -168,10 +216,14 @@ export class BackupController {
this._saveBlob(blob, fileName);
}
+ /**
+ * @param {File} file
+ * @returns {Promise<ArrayBuffer>}
+ */
_readFileArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
+ reader.onload = () => resolve(/** @type {ArrayBuffer} */ (reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
@@ -179,19 +231,33 @@ export class BackupController {
// Importing
+ /**
+ * @param {import('settings').Options} optionsFull
+ */
async _settingsImportSetOptionsFull(optionsFull) {
await this._settingsController.setAllSettings(optionsFull);
}
+ /**
+ * @param {Error} error
+ */
_showSettingsImportError(error) {
log.error(error);
- document.querySelector('#settings-import-error-message').textContent = `${error}`;
- this._settingsImportErrorModal.setVisible(true);
+ const element = /** @type {HTMLElement} */ (document.querySelector('#settings-import-error-message'));
+ element.textContent = `${error}`;
+ if (this._settingsImportErrorModal !== null) {
+ this._settingsImportErrorModal.setVisible(true);
+ }
}
+ /**
+ * @param {Set<string>} warnings
+ * @returns {Promise<import('backup-controller').ShowSettingsImportWarningsResult>}
+ */
async _showSettingsImportWarnings(warnings) {
const modal = this._settingsImportWarningModal;
- const buttons = document.querySelectorAll('.settings-import-warning-import-button');
+ if (modal === null) { return {result: false}; }
+ const buttons = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.settings-import-warning-import-button'));
const messageContainer = document.querySelector('#settings-import-warning-message');
if (buttons.length === 0 || messageContainer === null) {
return {result: false};
@@ -212,20 +278,30 @@ export class BackupController {
// Wait for modal to close
return new Promise((resolve) => {
+ /**
+ * @param {MouseEvent} e
+ */
const onButtonClick = (e) => {
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
e.preventDefault();
complete({
result: true,
- sanitize: e.currentTarget.dataset.importSanitize === 'true'
+ sanitize: element.dataset.importSanitize === 'true'
});
modal.setVisible(false);
};
+ /**
+ * @param {import('panel-element').VisibilityChangedEvent} details
+ */
const onModalVisibilityChanged = ({visible}) => {
if (visible) { return; }
complete({result: false});
};
let completed = false;
+ /**
+ * @param {import('backup-controller').ShowSettingsImportWarningsResult} result
+ */
const complete = (result) => {
if (completed) { return; }
completed = true;
@@ -246,6 +322,10 @@ export class BackupController {
});
}
+ /**
+ * @param {string} urlString
+ * @returns {boolean}
+ */
_isLocalhostUrl(urlString) {
try {
const url = new URL(urlString);
@@ -266,6 +346,11 @@ export class BackupController {
return false;
}
+ /**
+ * @param {import('settings').ProfileOptions} options
+ * @param {boolean} dryRun
+ * @returns {string[]}
+ */
_settingsImportSanitizeProfileOptions(options, dryRun) {
const warnings = [];
@@ -308,6 +393,11 @@ export class BackupController {
return warnings;
}
+ /**
+ * @param {import('settings').Options} optionsFull
+ * @param {boolean} dryRun
+ * @returns {Set<string>}
+ */
_settingsImportSanitizeOptions(optionsFull, dryRun) {
const warnings = new Set();
@@ -328,7 +418,12 @@ export class BackupController {
return warnings;
}
+ /**
+ * @param {File} file
+ */
async _importSettingsFile(file) {
+ if (this._optionsUtil === null) { throw new Error('OptionsUtil invalid'); }
+
const dataString = ArrayBufferUtil.arrayBufferUtf8Decode(await this._readFileArrayBuffer(file));
const data = JSON.parse(dataString);
@@ -383,31 +478,44 @@ export class BackupController {
await this._settingsImportSetOptionsFull(optionsFull);
}
+ /** */
_onSettingsImportClick() {
- document.querySelector('#settings-import-file').click();
+ const element = /** @type {HTMLElement} */ (document.querySelector('#settings-import-file'));
+ element.click();
}
+ /**
+ * @param {Event} e
+ */
async _onSettingsImportFileChange(e) {
- const files = e.target.files;
- if (files.length === 0) { return; }
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const files = element.files;
+ if (files === null || files.length === 0) { return; }
const file = files[0];
- e.target.value = null;
+ element.value = '';
try {
await this._importSettingsFile(file);
} catch (error) {
- this._showSettingsImportError(error);
+ this._showSettingsImportError(error instanceof Error ? error : new Error(`${error}`));
}
}
// Resetting
+ /** */
_onSettingsResetClick() {
+ if (this._settingsResetModal === null) { return; }
this._settingsResetModal.setVisible(true);
}
+ /** */
async _onSettingsResetConfirmClick() {
- this._settingsResetModal.setVisible(false);
+ if (this._optionsUtil === null) { throw new Error('OptionsUtil invalid'); }
+
+ if (this._settingsResetModal !== null) {
+ this._settingsResetModal.setVisible(false);
+ }
// Get default options
const optionsFull = this._optionsUtil.getDefault();
@@ -425,8 +533,12 @@ export class BackupController {
// Exporting Dictionaries Database
+ /**
+ * @param {string} message
+ * @param {boolean} [isWarning]
+ */
_databaseExportImportErrorMessage(message, isWarning=false) {
- const errorMessageContainer = document.querySelector('#db-ops-error-report');
+ const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
errorMessageContainer.style.display = 'block';
errorMessageContainer.textContent = message;
@@ -439,9 +551,12 @@ export class BackupController {
}
}
+ /**
+ * @param {{totalRows: number, completedRows: number, done: boolean}} details
+ */
_databaseExportProgressCallback({totalRows, completedRows, done}) {
console.log(`Progress: ${completedRows} of ${totalRows} rows completed`);
- const messageContainer = document.querySelector('#db-ops-progress-report');
+ const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report'));
messageContainer.style.display = 'block';
messageContainer.textContent = `Export Progress: ${completedRows} of ${totalRows} rows completed`;
@@ -451,6 +566,10 @@ export class BackupController {
}
}
+ /**
+ * @param {string} databaseName
+ * @returns {Promise<Blob>}
+ */
async _exportDatabase(databaseName) {
const db = await new Dexie(databaseName).open();
const blob = await db.export({progressCallback: this._databaseExportProgressCallback});
@@ -458,6 +577,9 @@ export class BackupController {
return blob;
}
+ /**
+ *
+ */
async _onSettingsExportDatabaseClick() {
if (this._settingsExportDatabaseToken !== null) {
// An existing import or export is in progress.
@@ -465,7 +587,7 @@ export class BackupController {
return;
}
- const errorMessageContainer = document.querySelector('#db-ops-error-report');
+ const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
errorMessageContainer.style.display = 'none';
const date = new Date(Date.now());
@@ -488,9 +610,12 @@ export class BackupController {
// Importing Dictionaries Database
+ /**
+ * @param {{totalRows: number, completedRows: number, done: boolean}} details
+ */
_databaseImportProgressCallback({totalRows, completedRows, done}) {
console.log(`Progress: ${completedRows} of ${totalRows} rows completed`);
- const messageContainer = document.querySelector('#db-ops-progress-report');
+ const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report'));
messageContainer.style.display = 'block';
messageContainer.style.color = '#4169e1';
messageContainer.textContent = `Import Progress: ${completedRows} of ${totalRows} rows completed`;
@@ -502,6 +627,10 @@ export class BackupController {
}
}
+ /**
+ * @param {string} databaseName
+ * @param {File} file
+ */
async _importDatabase(databaseName, file) {
await yomitan.api.purgeDatabase();
await Dexie.import(file, {progressCallback: this._databaseImportProgressCallback});
@@ -509,10 +638,14 @@ export class BackupController {
yomitan.trigger('storageChanged');
}
+ /** */
_onSettingsImportDatabaseClick() {
- document.querySelector('#settings-import-db').click();
+ /** @type {HTMLElement} */ (document.querySelector('#settings-import-db')).click();
}
+ /**
+ * @param {Event} e
+ */
async _onSettingsImportDatabaseChange(e) {
if (this._settingsExportDatabaseToken !== null) {
// An existing import or export is in progress.
@@ -520,22 +653,23 @@ export class BackupController {
return;
}
- const errorMessageContainer = document.querySelector('#db-ops-error-report');
+ const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
errorMessageContainer.style.display = 'none';
- const files = e.target.files;
- if (files.length === 0) { return; }
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const files = element.files;
+ if (files === null || files.length === 0) { return; }
const pageExitPrevention = this._settingsController.preventPageExit();
const file = files[0];
- e.target.value = null;
+ element.value = '';
try {
const token = {};
this._settingsExportDatabaseToken = token;
await this._importDatabase(this._dictionariesDatabaseName, file);
} catch (error) {
console.log(error);
- const messageContainer = document.querySelector('#db-ops-progress-report');
+ const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#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 c8ce5e4f..355292ad 100644
--- a/ext/js/pages/settings/collapsible-dictionary-controller.js
+++ b/ext/js/pages/settings/collapsible-dictionary-controller.js
@@ -20,18 +20,29 @@ import {EventListenerCollection} from '../../core.js';
import {yomitan} from '../../yomitan.js';
export class CollapsibleDictionaryController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?import('core').TokenObject} */
this._getDictionaryInfoToken = null;
+ /** @type {Map<string, import('dictionary-importer').Summary>} */
this._dictionaryInfoMap = new Map();
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?HTMLElement} */
this._container = null;
+ /** @type {HTMLSelectElement[]} */
this._selects = [];
+ /** @type {?HTMLSelectElement} */
this._allSelect = null;
}
+ /** */
async prepare() {
- this._container = document.querySelector('#collapsible-dictionary-list');
+ this._container = /** @type {HTMLElement} */ (document.querySelector('#collapsible-dictionary-list'));
await this._onDatabaseUpdated();
@@ -42,7 +53,9 @@ export class CollapsibleDictionaryController {
// Private
+ /** */
async _onDatabaseUpdated() {
+ /** @type {?import('core').TokenObject} */
const token = {};
this._getDictionaryInfoToken = token;
const dictionaries = await this._settingsController.getDictionaryInfo();
@@ -54,10 +67,12 @@ export class CollapsibleDictionaryController {
this._dictionaryInfoMap.set(entry.title, entry);
}
- const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ await this._onDictionarySettingsReordered();
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
this._eventListeners.removeAllEventListeners();
this._selects = [];
@@ -79,25 +94,37 @@ export class CollapsibleDictionaryController {
this._selects.push(select);
}
- this._container.textContent = '';
- this._container.appendChild(fragment);
+ const container = /** @type {HTMLElement} */ (this._container);
+ container.textContent = '';
+ container.appendChild(fragment);
}
+ /** */
_onDefinitionsCollapsibleChange() {
this._updateAllSelectFresh();
}
+ /**
+ * @param {Event} e
+ */
_onAllSelectChange(e) {
- const {value} = e.currentTarget;
- if (value === 'varies') { return; }
- this._setDefinitionsCollapsibleAll(value);
+ const {value} = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const value2 = this._normalizeDictionaryDefinitionsCollapsible(value);
+ if (value2 === null) { return; }
+ this._setDefinitionsCollapsibleAll(value2);
}
+ /** */
async _onDictionarySettingsReordered() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /**
+ * @param {DocumentFragment} fragment
+ * @param {import('settings').ProfileOptions} options
+ */
_setupAllSelect(fragment, options) {
const select = this._addSelect(fragment, 'All', '');
@@ -113,23 +140,33 @@ export class CollapsibleDictionaryController {
this._updateAllSelect(options);
}
+ /**
+ * @param {DocumentFragment} fragment
+ * @param {string} dictionary
+ * @param {string} version
+ * @returns {HTMLSelectElement}
+ */
_addSelect(fragment, dictionary, version) {
const node = this._settingsController.instantiateTemplate('collapsible-dictionary-item');
fragment.appendChild(node);
- const nameNode = node.querySelector('.dictionary-title');
+ const nameNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-title'));
nameNode.textContent = dictionary;
- const versionNode = node.querySelector('.dictionary-version');
+ const versionNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-version'));
versionNode.textContent = version;
- return node.querySelector('.definitions-collapsible');
+ return /** @type {HTMLSelectElement} */ (node.querySelector('.definitions-collapsible'));
}
+ /** */
async _updateAllSelectFresh() {
this._updateAllSelect(await this._settingsController.getOptions());
}
+ /**
+ * @param {import('settings').ProfileOptions} options
+ */
_updateAllSelect(options) {
let value = null;
let varies = false;
@@ -142,11 +179,17 @@ export class CollapsibleDictionaryController {
}
}
- this._allSelect.value = (varies || value === null ? 'varies' : value);
+ if (this._allSelect !== null) {
+ this._allSelect.value = (varies || value === null ? 'varies' : value);
+ }
}
+ /**
+ * @param {import('settings').DictionaryDefinitionsCollapsible} value
+ */
async _setDefinitionsCollapsibleAll(value) {
const options = await this._settingsController.getOptions();
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
const {dictionaries} = options;
for (let i = 0, ii = dictionaries.length; i < ii; ++i) {
@@ -158,4 +201,21 @@ export class CollapsibleDictionaryController {
select.value = value;
}
}
+
+ /**
+ * @param {string} value
+ * @returns {?import('settings').DictionaryDefinitionsCollapsible}
+ */
+ _normalizeDictionaryDefinitionsCollapsible(value) {
+ switch (value) {
+ case 'not-collapsible':
+ case 'expanded':
+ case 'collapsed':
+ case 'force-collapsed':
+ case 'force-expanded':
+ return value;
+ default:
+ return null;
+ }
+ }
}
diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js
index 155ce55e..de63b200 100644
--- a/ext/js/pages/settings/dictionary-controller.js
+++ b/ext/js/pages/settings/dictionary-controller.js
@@ -21,27 +21,49 @@ import {DictionaryWorker} from '../../language/dictionary-worker.js';
import {yomitan} from '../../yomitan.js';
class DictionaryEntry {
+ /**
+ * @param {DictionaryController} dictionaryController
+ * @param {DocumentFragment} fragment
+ * @param {number} index
+ * @param {import('dictionary-importer').Summary} dictionaryInfo
+ */
constructor(dictionaryController, fragment, index, dictionaryInfo) {
+ /** @type {DictionaryController} */
this._dictionaryController = dictionaryController;
+ /** @type {number} */
this._index = index;
+ /** @type {import('dictionary-importer').Summary} */
this._dictionaryInfo = dictionaryInfo;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?import('dictionary-database').DictionaryCountGroup} */
this._counts = null;
+ /** @type {ChildNode[]} */
this._nodes = [...fragment.childNodes];
- this._enabledCheckbox = fragment.querySelector('.dictionary-enabled');
- this._priorityInput = fragment.querySelector('.dictionary-priority');
- this._menuButton = fragment.querySelector('.dictionary-menu-button');
- this._outdatedButton = fragment.querySelector('.dictionary-outdated-button');
- this._integrityButton = fragment.querySelector('.dictionary-integrity-button');
- this._titleNode = fragment.querySelector('.dictionary-title');
- this._versionNode = fragment.querySelector('.dictionary-version');
- this._titleContainer = fragment.querySelector('.dictionary-item-title-container');
- }
-
+ /** @type {HTMLInputElement} */
+ this._enabledCheckbox = /** @type {HTMLInputElement} */ (fragment.querySelector('.dictionary-enabled'));
+ /** @type {HTMLInputElement} */
+ this._priorityInput = /** @type {HTMLInputElement} */ (fragment.querySelector('.dictionary-priority'));
+ /** @type {HTMLButtonElement} */
+ this._menuButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-menu-button'));
+ /** @type {HTMLButtonElement} */
+ this._outdatedButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-outdated-button'));
+ /** @type {HTMLButtonElement} */
+ this._integrityButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-integrity-button'));
+ /** @type {HTMLElement} */
+ this._titleNode = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-title'));
+ /** @type {HTMLElement} */
+ this._versionNode = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-version'));
+ /** @type {HTMLElement} */
+ this._titleContainer = /** @type {HTMLElement} */ (fragment.querySelector('.dictionary-item-title-container'));
+ }
+
+ /** @type {string} */
get dictionaryTitle() {
return this._dictionaryInfo.title;
}
+ /** */
prepare() {
const index = this._index;
const {title, revision, version} = this._dictionaryInfo;
@@ -58,6 +80,7 @@ class DictionaryEntry {
this._eventListeners.addEventListener(this._integrityButton, 'click', this._onIntegrityButtonClick.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
for (const node of this._nodes) {
@@ -68,17 +91,26 @@ class DictionaryEntry {
this._nodes = [];
}
+ /**
+ * @param {import('dictionary-database').DictionaryCountGroup} counts
+ */
setCounts(counts) {
this._counts = counts;
this._integrityButton.hidden = false;
}
+ /**
+ * @param {boolean} value
+ */
setEnabled(value) {
this._enabledCheckbox.checked = value;
}
// Private
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const bodyNode = e.detail.menu.bodyNode;
const count = this._dictionaryController.dictionaryOptionCount;
@@ -87,6 +119,9 @@ class DictionaryEntry {
this._setMenuActionEnabled(bodyNode, 'moveTo', count > 1);
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'delete':
@@ -107,36 +142,48 @@ class DictionaryEntry {
}
}
+ /**
+ * @param {import('dom-data-binder').SettingChangedEvent} e
+ */
_onEnabledChanged(e) {
const {detail: {value}} = e;
this._titleContainer.dataset.enabled = `${value}`;
this._dictionaryController.updateDictionariesEnabled();
}
+ /** */
_onOutdatedButtonClick() {
this._showDetails();
}
+ /** */
_onIntegrityButtonClick() {
this._showDetails();
}
+ /** */
_showDetails() {
const {title, revision, version, prefixWildcardsSupported} = this._dictionaryInfo;
const modal = this._dictionaryController.modalController.getModal('dictionary-details');
+ if (modal === null) { return; }
- modal.node.querySelector('.dictionary-title').textContent = title;
- modal.node.querySelector('.dictionary-version').textContent = `rev.${revision}`;
- modal.node.querySelector('.dictionary-outdated-notification').hidden = (version >= 3);
- modal.node.querySelector('.dictionary-counts').textContent = this._counts !== null ? JSON.stringify(this._counts, null, 4) : '';
- modal.node.querySelector('.dictionary-prefix-wildcard-searches-supported').checked = prefixWildcardsSupported;
- this._setupDetails(modal.node.querySelector('.dictionary-details-table'));
+ /** @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')));
modal.setVisible(true);
}
+ /**
+ * @param {Element} detailsTable
+ * @returns {boolean}
+ */
_setupDetails(detailsTable) {
+ /** @type {[label: string, key: 'author'|'url'|'description'|'attribution'][]} */
const targets = [
['Author', 'author'],
['URL', 'url'],
@@ -151,10 +198,10 @@ class DictionaryEntry {
const info = dictionaryInfo[key];
if (typeof info !== 'string') { continue; }
- const details = this._dictionaryController.instantiateTemplate('dictionary-details-entry');
+ const details = /** @type {HTMLElement} */ (this._dictionaryController.instantiateTemplate('dictionary-details-entry'));
details.dataset.type = key;
- details.querySelector('.dictionary-details-entry-label').textContent = `${label}:`;
- details.querySelector('.dictionary-details-entry-info').textContent = info;
+ /** @type {HTMLElement} */ (details.querySelector('.dictionary-details-entry-label')).textContent = `${label}:`;
+ /** @type {HTMLElement} */ (details.querySelector('.dictionary-details-entry-info')).textContent = info;
fragment.appendChild(details);
any = true;
@@ -165,28 +212,40 @@ class DictionaryEntry {
return any;
}
+ /** */
_delete() {
this._dictionaryController.deleteDictionary(this.dictionaryTitle);
}
+ /**
+ * @param {number} offset
+ */
_move(offset) {
this._dictionaryController.moveDictionaryOptions(this._index, this._index + offset);
}
+ /**
+ * @param {Element} menu
+ * @param {string} action
+ * @param {boolean} enabled
+ */
_setMenuActionEnabled(menu, action, enabled) {
- const element = menu.querySelector(`[data-menu-action="${action}"]`);
+ const element = /** @type {?HTMLButtonElement} */ (menu.querySelector(`[data-menu-action="${action}"]`));
if (element === null) { return; }
element.disabled = !enabled;
}
+ /** */
_showMoveToModal() {
const {title} = this._dictionaryInfo;
const count = this._dictionaryController.dictionaryOptionCount;
const modal = this._dictionaryController.modalController.getModal('dictionary-move-location');
- const input = modal.node.querySelector('#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'));
modal.node.dataset.index = `${this._index}`;
- modal.node.querySelector('.dictionary-title').textContent = title;
+ titleNode.textContent = title;
input.value = `${this._index + 1}`;
input.max = `${count}`;
@@ -195,25 +254,45 @@ class DictionaryEntry {
}
class DictionaryExtraInfo {
+ /**
+ * @param {DictionaryController} parent
+ * @param {import('dictionary-database').DictionaryCountGroup} totalCounts
+ * @param {import('dictionary-database').DictionaryCountGroup} remainders
+ * @param {number} totalRemainder
+ */
constructor(parent, totalCounts, remainders, totalRemainder) {
+ /** @type {DictionaryController} */
this._parent = parent;
+ /** @type {import('dictionary-database').DictionaryCountGroup} */
this._totalCounts = totalCounts;
+ /** @type {import('dictionary-database').DictionaryCountGroup} */
this._remainders = remainders;
+ /** @type {number} */
this._totalRemainder = totalRemainder;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
- this._nodes = null;
+ /** @type {ChildNode[]} */
+ this._nodes = [];
}
+ /**
+ * @param {HTMLElement} container
+ */
prepare(container) {
const fragment = this._parent.instantiateTemplateFragment('dictionary-extra');
- this._nodes = [...fragment.childNodes];
+ for (const node of fragment.childNodes) {
+ this._nodes.push(node);
+ }
+
+ const dictionaryIntegrityButton = /** @type {HTMLButtonElement} */ (fragment.querySelector('.dictionary-integrity-button'));
this._setTitle(fragment.querySelector('.dictionary-total-count'));
- this._eventListeners.addEventListener(fragment.querySelector('.dictionary-integrity-button'), 'click', this._onIntegrityButtonClick.bind(this), false);
+ this._eventListeners.addEventListener(dictionaryIntegrityButton, 'click', this._onIntegrityButtonClick.bind(this), false);
container.appendChild(fragment);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
for (const node of this._nodes) {
@@ -221,74 +300,110 @@ class DictionaryExtraInfo {
node.parentNode.removeChild(node);
}
}
- this._nodes = [];
+ this._nodes.length =0;
}
// Private
+ /** */
_onIntegrityButtonClick() {
this._showDetails();
}
+ /** */
_showDetails() {
const modal = this._parent.modalController.getModal('dictionary-extra-data');
+ if (modal === null) { return; }
+
+ const dictionaryCounts = /** @type {HTMLElement} */ (modal.node.querySelector('.dictionary-counts'));
const info = {counts: this._totalCounts, remainders: this._remainders};
- modal.node.querySelector('.dictionary-counts').textContent = JSON.stringify(info, null, 4);
+ dictionaryCounts.textContent = JSON.stringify(info, null, 4);
this._setTitle(modal.node.querySelector('.dictionary-total-count'));
modal.setVisible(true);
}
+ /**
+ * @param {?Element} node
+ */
_setTitle(node) {
+ if (node === null) { return; }
node.textContent = `${this._totalRemainder} item${this._totalRemainder !== 1 ? 's' : ''}`;
}
}
export class DictionaryController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ * @param {import('./status-footer.js').StatusFooter} statusFooter
+ */
constructor(settingsController, modalController, statusFooter) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {import('./status-footer.js').StatusFooter} */
this._statusFooter = statusFooter;
+ /** @type {?import('dictionary-importer').Summary[]} */
this._dictionaries = null;
+ /** @type {DictionaryEntry[]} */
this._dictionaryEntries = [];
+ /** @type {?import('core').TokenObject} */
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 {?NodeListOf<HTMLElement>} */
this._noDictionariesInstalledWarnings = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._noDictionariesEnabledWarnings = null;
+ /** @type {?import('./modal.js').Modal} */
this._deleteDictionaryModal = null;
+ /** @type {?HTMLInputElement} */
this._allCheckbox = null;
+ /** @type {?DictionaryExtraInfo} */
this._extraInfo = null;
+ /** @type {boolean} */
this._isDeleting = false;
}
+ /** @type {import('./modal-controller.js').ModalController} */
get modalController() {
return this._modalController;
}
+ /** @type {number} */
get dictionaryOptionCount() {
return this._dictionaryEntries.length;
}
+ /** */
async prepare() {
- this._checkIntegrityButton = document.querySelector('#dictionary-check-integrity');
- this._dictionaryEntryContainer = document.querySelector('#dictionary-list');
- this._dictionaryInstallCountNode = document.querySelector('#dictionary-install-count');
- this._dictionaryEnabledCountNode = document.querySelector('#dictionary-enabled-count');
- this._noDictionariesInstalledWarnings = document.querySelectorAll('.no-dictionaries-installed-warning');
- this._noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning');
+ 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<HTMLElement>} */ (document.querySelectorAll('.no-dictionaries-installed-warning'));
+ this._noDictionariesEnabledWarnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.no-dictionaries-enabled-warning'));
this._deleteDictionaryModal = this._modalController.getModal('dictionary-confirm-delete');
- this._allCheckbox = document.querySelector('#all-dictionaries-enabled');
+ 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'));
yomitan.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
this._allCheckbox.addEventListener('change', this._onAllCheckboxChange.bind(this), false);
- document.querySelector('#dictionary-confirm-delete-button').addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false);
- document.querySelector('#dictionary-move-button').addEventListener('click', this._onDictionaryMoveButtonClick.bind(this), false);
+ dictionaryDeleteButton.addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false);
+ dictionaryMoveButton.addEventListener('click', this._onDictionaryMoveButtonClick.bind(this), false);
if (this._checkIntegrityButton !== null) {
this._checkIntegrityButton.addEventListener('click', this._onCheckIntegrityButtonClick.bind(this), false);
}
@@ -298,14 +413,22 @@ export class DictionaryController {
await this._onDatabaseUpdated();
}
+ /**
+ * @param {string} dictionaryTitle
+ */
deleteDictionary(dictionaryTitle) {
if (this._isDeleting) { return; }
- const modal = this._deleteDictionaryModal;
+ const modal = /** @type {import('./modal.js').Modal} */ (this._deleteDictionaryModal);
modal.node.dataset.dictionaryTitle = dictionaryTitle;
- modal.node.querySelector('#dictionary-confirm-delete-name').textContent = dictionaryTitle;
+ const nameElement = /** @type {Element} */ (modal.node.querySelector('#dictionary-confirm-delete-name'));
+ nameElement.textContent = dictionaryTitle;
modal.setVisible(true);
}
+ /**
+ * @param {number} currentIndex
+ * @param {number} targetIndex
+ */
async moveDictionaryOptions(currentIndex, targetIndex) {
const options = await this._settingsController.getOptions();
const {dictionaries} = options;
@@ -326,24 +449,40 @@ export class DictionaryController {
value: dictionaries
}]);
- this._settingsController.trigger('dictionarySettingsReordered', {source: this});
+ /** @type {import('settings-controller').DictionarySettingsReorderedEvent} */
+ const event = {source: this};
+ this._settingsController.trigger('dictionarySettingsReordered', event);
await this._updateEntries();
}
+ /**
+ * @param {string} name
+ * @returns {Element}
+ */
instantiateTemplate(name) {
return this._settingsController.instantiateTemplate(name);
}
+ /**
+ * @param {string} name
+ * @returns {DocumentFragment}
+ */
instantiateTemplateFragment(name) {
return this._settingsController.instantiateTemplateFragment(name);
}
+ /** */
async updateDictionariesEnabled() {
const options = await this._settingsController.getOptions();
this._updateDictionariesEnabledWarnings(options);
}
+ /**
+ * @param {string} name
+ * @param {boolean} enabled
+ * @returns {import('settings').DictionaryOptions}
+ */
static createDefaultDictionarySettings(name, enabled) {
return {
name,
@@ -354,6 +493,13 @@ export class DictionaryController {
};
}
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('dictionary-importer').Summary[]|undefined} dictionaries
+ * @param {import('settings').Options|undefined} optionsFull
+ * @param {boolean} modifyGlobalSettings
+ * @param {boolean} newDictionariesEnabled
+ */
static async ensureDictionarySettings(settingsController, dictionaries, optionsFull, modifyGlobalSettings, newDictionariesEnabled) {
if (typeof dictionaries === 'undefined') {
dictionaries = await settingsController.getDictionaryInfo();
@@ -367,6 +513,7 @@ export class DictionaryController {
installedDictionaries.add(title);
}
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
const {profiles} = optionsFull;
for (let i = 0, ii = profiles.length; i < ii; ++i) {
@@ -405,6 +552,9 @@ export class DictionaryController {
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
this._updateDictionariesEnabledWarnings(options);
if (this._dictionaries !== null) {
@@ -412,7 +562,9 @@ export class DictionaryController {
}
}
+ /** */
async _onDatabaseUpdated() {
+ /** @type {?import('core').TokenObject} */
const token = {};
this._databaseStateToken = token;
this._dictionaries = null;
@@ -423,14 +575,18 @@ export class DictionaryController {
await this._updateEntries();
}
+ /** */
_onAllCheckboxChange() {
- const value = this._allCheckbox.checked;
- this._allCheckbox.checked = !value;
+ const allCheckbox = /** @type {HTMLInputElement} */ (this._allCheckbox);
+ const value = allCheckbox.checked;
+ allCheckbox.checked = !value;
this._setAllDictionariesEnabled(value);
}
+ /** */
async _updateEntries() {
const dictionaries = this._dictionaries;
+ if (dictionaries === null) { return; }
this._updateMainDictionarySelectOptions(dictionaries);
for (const entry of this._dictionaryEntries) {
@@ -444,7 +600,7 @@ export class DictionaryController {
}
const hasDictionary = (dictionaries.length > 0);
- for (const node of this._noDictionariesInstalledWarnings) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._noDictionariesInstalledWarnings)) {
node.hidden = hasDictionary;
}
@@ -453,8 +609,9 @@ export class DictionaryController {
const options = await this._settingsController.getOptions();
this._updateDictionariesEnabledWarnings(options);
+ /** @type {Map<string, import('dictionary-importer').Summary>} */
const dictionaryInfoMap = new Map();
- for (const dictionary of this._dictionaries) {
+ for (const dictionary of dictionaries) {
dictionaryInfoMap.set(dictionary.title, dictionary);
}
@@ -467,6 +624,9 @@ export class DictionaryController {
}
}
+ /**
+ * @param {import('settings').ProfileOptions} options
+ */
_updateDictionariesEnabledWarnings(options) {
const {dictionaries} = options;
let enabledDictionaryCountValid = 0;
@@ -489,7 +649,7 @@ export class DictionaryController {
}
const hasEnabledDictionary = (enabledDictionaryCountValid > 0);
- for (const node of this._noDictionariesEnabledWarnings) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._noDictionariesEnabledWarnings)) {
node.hidden = hasEnabledDictionary;
}
@@ -497,7 +657,7 @@ export class DictionaryController {
this._dictionaryEnabledCountNode.textContent = `${enabledDictionaryCountValid}`;
}
- this._allCheckbox.checked = (enabledDictionaryCount >= dictionaryCount);
+ /** @type {HTMLInputElement} */ (this._allCheckbox).checked = (enabledDictionaryCount >= dictionaryCount);
const entries = this._dictionaryEntries;
for (let i = 0, ii = Math.min(entries.length, dictionaryCount); i < ii; ++i) {
@@ -505,10 +665,13 @@ export class DictionaryController {
}
}
+ /**
+ * @param {MouseEvent} e
+ */
_onDictionaryConfirmDelete(e) {
e.preventDefault();
- const modal = this._deleteDictionaryModal;
+ const modal = /** @type {import('./modal.js').Modal} */ (this._deleteDictionaryModal);
modal.setVisible(false);
const title = modal.node.dataset.dictionaryTitle;
@@ -518,24 +681,32 @@ export class DictionaryController {
this._deleteDictionary(title);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onCheckIntegrityButtonClick(e) {
e.preventDefault();
this._checkIntegrity();
}
+ /** */
_onDictionaryMoveButtonClick() {
- const modal = this._modalController.getModal('dictionary-move-location');
- let {index} = modal.node.dataset;
- index = Number.parseInt(index, 10);
+ const modal = /** @type {import('./modal.js').Modal} */ (this._modalController.getModal('dictionary-move-location'));
+ const {index} = modal.node.dataset;
+ if (typeof index !== 'number') { return; }
+ const indexNumber = Number.parseInt(index, 10);
- let target = document.querySelector('#dictionary-move-location').value;
- target = Number.parseInt(target, 10) - 1;
+ const targetString = /** @type {HTMLInputElement} */ (document.querySelector('#dictionary-move-location')).value;
+ const target = Number.parseInt(targetString, 10) - 1;
- if (!Number.isFinite(target) || !Number.isFinite(index) || index === target) { return; }
+ if (!Number.isFinite(target) || !Number.isFinite(indexNumber) || indexNumber === target) { return; }
- this.moveDictionaryOptions(index, target);
+ this.moveDictionaryOptions(indexNumber, target);
}
+ /**
+ * @param {import('dictionary-importer').Summary[]} dictionaries
+ */
_updateMainDictionarySelectOptions(dictionaries) {
for (const select of document.querySelectorAll('[data-setting="general.mainDictionary"]')) {
const fragment = document.createDocumentFragment();
@@ -559,6 +730,7 @@ export class DictionaryController {
}
}
+ /** */
async _checkIntegrity() {
if (this._dictionaries === null || this._checkingIntegrity || this._isDeleting) { return; }
@@ -576,13 +748,17 @@ export class DictionaryController {
entry.setCounts(counts[i]);
}
- this._setCounts(counts, total);
+ this._setCounts(counts, /** @type {import('dictionary-database').DictionaryCountGroup} */ (total));
} finally {
this._setButtonsEnabled(true);
this._checkingIntegrity = false;
}
}
+ /**
+ * @param {import('dictionary-database').DictionaryCountGroup[]} dictionaryCounts
+ * @param {import('dictionary-database').DictionaryCountGroup} totalCounts
+ */
_setCounts(dictionaryCounts, totalCounts) {
const remainders = Object.assign({}, totalCounts);
const keys = Object.keys(remainders);
@@ -603,12 +779,16 @@ export class DictionaryController {
this._extraInfo = null;
}
- if (totalRemainder > 0) {
+ if (totalRemainder > 0 && this._dictionaryEntryContainer !== null) {
this._extraInfo = new DictionaryExtraInfo(this, totalCounts, remainders, totalRemainder);
this._extraInfo.prepare(this._dictionaryEntryContainer);
}
}
+ /**
+ * @param {number} index
+ * @param {import('dictionary-importer').Summary} dictionaryInfo
+ */
_createDictionaryEntry(index, dictionaryInfo) {
const fragment = this.instantiateTemplateFragment('dictionary');
@@ -616,13 +796,16 @@ export class DictionaryController {
this._dictionaryEntries.push(entry);
entry.prepare();
- const container = this._dictionaryEntryContainer;
+ const container = /** @type {HTMLElement} */ (this._dictionaryEntryContainer);
const relative = container.querySelector('.dictionary-item-bottom');
container.insertBefore(fragment, relative);
this._updateDictionaryEntryCount();
}
+ /**
+ * @param {string} dictionaryTitle
+ */
async _deleteDictionary(dictionaryTitle) {
if (this._isDeleting || this._checkingIntegrity) { return; }
@@ -631,15 +814,18 @@ export class DictionaryController {
const statusFooter = this._statusFooter;
const progressSelector = '.dictionary-delete-progress';
- const progressContainers = document.querySelectorAll(`#dictionaries-modal ${progressSelector}`);
- const progressBars = document.querySelectorAll(`${progressSelector} .progress-bar`);
- const infoLabels = document.querySelectorAll(`${progressSelector} .progress-info`);
- const statusLabels = document.querySelectorAll(`${progressSelector} .progress-status`);
+ const progressContainers = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`#dictionaries-modal ${progressSelector}`));
+ const progressBars = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-bar`));
+ const infoLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-info`));
+ const statusLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-status`));
const prevention = this._settingsController.preventPageExit();
try {
this._isDeleting = true;
this._setButtonsEnabled(false);
+ /**
+ * @param {import('dictionary-database').DeleteDictionaryProgressData} details
+ */
const onProgress = ({processed, count, storeCount, storesProcesed}) => {
const percent = (
(count > 0 && storesProcesed > 0) ?
@@ -672,21 +858,32 @@ export class DictionaryController {
}
}
+ /**
+ * @param {boolean} value
+ */
_setButtonsEnabled(value) {
value = !value;
- for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) {
+ for (const node of /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('.dictionary-database-mutating-input'))) {
node.disabled = value;
}
}
+ /**
+ * @param {string} dictionaryTitle
+ * @param {import('dictionary-worker').DeleteProgressCallback} onProgress
+ */
async _deleteDictionaryInternal(dictionaryTitle, onProgress) {
await new DictionaryWorker().deleteDictionary(dictionaryTitle, onProgress);
yomitan.api.triggerDatabaseUpdated('dictionary', 'delete');
}
+ /**
+ * @param {string} dictionaryTitle
+ */
async _deleteDictionarySettings(dictionaryTitle) {
const optionsFull = await this._settingsController.getOptionsFull();
const {profiles} = optionsFull;
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
for (let i = 0, ii = profiles.length; i < ii; ++i) {
const {options: {dictionaries}} = profiles[i];
@@ -705,18 +902,24 @@ export class DictionaryController {
await this._settingsController.modifyGlobalSettings(targets);
}
+ /** */
_triggerStorageChanged() {
yomitan.trigger('storageChanged');
}
+ /** */
_updateDictionaryEntryCount() {
- this._dictionaryEntryContainer.dataset.count = `${this._dictionaryEntries.length}`;
+ /** @type {HTMLElement} */ (this._dictionaryEntryContainer).dataset.count = `${this._dictionaryEntries.length}`;
}
+ /**
+ * @param {boolean} value
+ */
async _setAllDictionariesEnabled(value) {
const options = await this._settingsController.getOptions();
const {dictionaries} = options;
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
for (let i = 0, ii = dictionaries.length; i < ii; ++i) {
targets.push({
diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js
index 12d29a6f..d1255e11 100644
--- a/ext/js/pages/settings/dictionary-import-controller.js
+++ b/ext/js/pages/settings/dictionary-import-controller.js
@@ -16,25 +16,44 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {deserializeError, log} from '../../core.js';
+import {log} from '../../core.js';
+import {ExtensionError} from '../../core/extension-error.js';
import {DictionaryWorker} from '../../language/dictionary-worker.js';
import {yomitan} from '../../yomitan.js';
import {DictionaryController} from './dictionary-controller.js';
export class DictionaryImportController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ * @param {import('./status-footer.js').StatusFooter} statusFooter
+ */
constructor(settingsController, modalController, statusFooter) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {import('./status-footer.js').StatusFooter} */
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 {?import('./modal.js').Modal} */
this._purgeConfirmModal = null;
+ /** @type {?HTMLElement} */
this._errorContainer = null;
+ /** @type {?HTMLElement} */
this._spinner = null;
+ /** @type {?HTMLElement} */
this._purgeNotification = null;
+ /** @type {[originalMessage: string, newMessage: string][]} */
this._errorToStringOverrides = [
[
'A mutation operation was attempted on a database that did not allow mutations.',
@@ -47,15 +66,16 @@ export class DictionaryImportController {
];
}
+ /** */
async prepare() {
- this._purgeButton = document.querySelector('#dictionary-delete-all-button');
- this._purgeConfirmButton = document.querySelector('#dictionary-confirm-delete-all-button');
- this._importFileButton = document.querySelector('#dictionary-import-file-button');
- this._importFileInput = document.querySelector('#dictionary-import-file-input');
+ 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 = document.querySelector('#dictionary-error');
- this._spinner = document.querySelector('#dictionary-spinner');
- this._purgeNotification = document.querySelector('#dictionary-delete-all-status');
+ 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);
@@ -65,28 +85,41 @@ export class DictionaryImportController {
// Private
+ /** */
_onImportButtonClick() {
- this._importFileInput.click();
+ /** @type {HTMLInputElement} */ (this._importFileInput).click();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onPurgeButtonClick(e) {
e.preventDefault();
- this._purgeConfirmModal.setVisible(true);
+ /** @type {import('./modal.js').Modal} */ (this._purgeConfirmModal).setVisible(true);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onPurgeConfirmButtonClick(e) {
e.preventDefault();
- this._purgeConfirmModal.setVisible(false);
+ /** @type {import('./modal.js').Modal} */ (this._purgeConfirmModal).setVisible(false);
this._purgeDatabase();
}
+ /**
+ * @param {Event} e
+ */
_onImportFileChange(e) {
- const node = e.currentTarget;
- const files = [...node.files];
- node.value = null;
- this._importDictionaries(files);
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const {files} = node;
+ if (files === null) { return; }
+ const files2 = [...files];
+ node.value = '';
+ this._importDictionaries(files2);
}
+ /** */
async _purgeDatabase() {
if (this._modifying) { return; }
@@ -106,7 +139,7 @@ export class DictionaryImportController {
this._showErrors(errors);
}
} catch (error) {
- this._showErrors([error]);
+ this._showErrors([error instanceof Error ? error : new Error(`${error}`)]);
} finally {
prevention.end();
if (purgeNotification !== null) { purgeNotification.hidden = true; }
@@ -116,16 +149,19 @@ export class DictionaryImportController {
}
}
+ /**
+ * @param {File[]} files
+ */
async _importDictionaries(files) {
if (this._modifying) { return; }
const statusFooter = this._statusFooter;
- const importInfo = document.querySelector('#dictionary-import-info');
+ const importInfo = /** @type {HTMLElement} */ (document.querySelector('#dictionary-import-info'));
const progressSelector = '.dictionary-import-progress';
- const progressContainers = document.querySelectorAll(`#dictionaries-modal ${progressSelector}`);
- const progressBars = document.querySelectorAll(`${progressSelector} .progress-bar`);
- const infoLabels = document.querySelectorAll(`${progressSelector} .progress-info`);
- const statusLabels = document.querySelectorAll(`${progressSelector} .progress-status`);
+ const progressContainers = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`#dictionaries-modal ${progressSelector}`));
+ const progressBars = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-bar`));
+ const infoLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-info`));
+ const statusLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-status`));
const prevention = this._preventPageExit();
@@ -143,6 +179,7 @@ export class DictionaryImportController {
let statusPrefix = '';
let stepIndex = -2;
+ /** @type {import('dictionary-worker').ImportProgressCallback} */
const onProgress = (data) => {
const {stepIndex: stepIndex2, index, count} = data;
if (stepIndex !== stepIndex2) {
@@ -184,7 +221,7 @@ export class DictionaryImportController {
await this._importDictionary(files[i], importDetails, onProgress);
}
} catch (err) {
- this._showErrors([err]);
+ this._showErrors([err instanceof Error ? err : new Error(`${err}`)]);
} finally {
prevention.end();
for (const progress of progressContainers) { progress.hidden = true; }
@@ -199,6 +236,10 @@ export class DictionaryImportController {
}
}
+ /**
+ * @param {number} stepIndex
+ * @returns {string}
+ */
_getImportLabel(stepIndex) {
switch (stepIndex) {
case -1:
@@ -212,6 +253,11 @@ export class DictionaryImportController {
}
}
+ /**
+ * @param {File} file
+ * @param {import('dictionary-importer').ImportDetails} importDetails
+ * @param {import('dictionary-worker').ImportProgressCallback} onProgress
+ */
async _importDictionary(file, importDetails, onProgress) {
const archiveContent = await this._readFile(file);
const {result, errors} = await new DictionaryWorker().importDictionary(archiveContent, importDetails, onProgress);
@@ -225,8 +271,14 @@ export class DictionaryImportController {
}
}
+ /**
+ * @param {boolean} sequenced
+ * @param {string} title
+ * @returns {Promise<Error[]>}
+ */
async _addDictionarySettings(sequenced, title) {
const optionsFull = await this._settingsController.getOptionsFull();
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
const profileCount = optionsFull.profiles.length;
for (let i = 0; i < profileCount; ++i) {
@@ -243,8 +295,12 @@ export class DictionaryImportController {
return await this._modifyGlobalSettings(targets);
}
+ /**
+ * @returns {Promise<Error[]>}
+ */
async _clearDictionarySettings() {
const optionsFull = await this._settingsController.getOptionsFull();
+ /** @type {import('settings-modifications').Modification[]} */
const targets = [];
const profileCount = optionsFull.profiles.length;
for (let i = 0; i < profileCount; ++i) {
@@ -256,16 +312,25 @@ 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}
+ */
_preventPageExit() {
return this._settingsController.preventPageExit();
}
+ /**
+ * @param {Error[]} errors
+ */
_showErrors(errors) {
const uniqueErrors = new Map();
for (const error of errors) {
@@ -292,59 +357,81 @@ export class DictionaryImportController {
fragment.appendChild(div);
}
- this._errorContainer.appendChild(fragment);
- this._errorContainer.hidden = false;
+ const errorContainer = /** @type {HTMLElement} */ (this._errorContainer);
+ errorContainer.appendChild(fragment);
+ errorContainer.hidden = false;
}
+ /** */
_hideErrors() {
- this._errorContainer.textContent = '';
- this._errorContainer.hidden = true;
+ const errorContainer = /** @type {HTMLElement} */ (this._errorContainer);
+ errorContainer.textContent = '';
+ errorContainer.hidden = true;
}
+ /**
+ * @param {File} file
+ * @returns {Promise<ArrayBuffer>}
+ */
_readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
+ reader.onload = () => resolve(/** @type {ArrayBuffer} */ (reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
+ /**
+ * @param {Error} error
+ * @returns {string}
+ */
_errorToString(error) {
- error = (typeof error.toString === 'function' ? error.toString() : `${error}`);
+ const errorMessage = error.toString();
for (const [match, newErrorString] of this._errorToStringOverrides) {
- if (error.includes(match)) {
+ if (errorMessage.includes(match)) {
return newErrorString;
}
}
- return error;
+ return errorMessage;
}
+ /**
+ * @param {boolean} value
+ */
_setModifying(value) {
this._modifying = value;
this._setButtonsEnabled(!value);
}
+ /**
+ * @param {boolean} value
+ */
_setButtonsEnabled(value) {
value = !value;
- for (const node of document.querySelectorAll('.dictionary-database-mutating-input')) {
+ for (const node of /** @type {NodeListOf<HTMLInputElement>} */ (document.querySelectorAll('.dictionary-database-mutating-input'))) {
node.disabled = value;
}
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<Error[]>}
+ */
async _modifyGlobalSettings(targets) {
const results = await this._settingsController.modifyGlobalSettings(targets);
const errors = [];
for (const {error} of results) {
if (typeof error !== 'undefined') {
- errors.push(deserializeError(error));
+ errors.push(ExtensionError.deserialize(error));
}
}
return errors;
}
+ /** */
_triggerStorageChanged() {
yomitan.trigger('storageChanged');
}
diff --git a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
index 4f3ed569..d36d965a 100644
--- a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
+++ b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
@@ -22,24 +22,36 @@ import {yomitan} from '../../yomitan.js';
import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';
export class ExtensionKeyboardShortcutController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
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 {HotkeyUtil} */
this._hotkeyUtil = new HotkeyUtil();
+ /** @type {?import('environment').OperatingSystem} */
this._os = null;
+ /** @type {ExtensionKeyboardShortcutHotkeyEntry[]} */
this._entries = [];
}
+ /** @type {HotkeyUtil} */
get hotkeyUtil() {
return this._hotkeyUtil;
}
+ /** */
async prepare() {
- this._resetButton = document.querySelector('#extension-hotkey-list-reset-all');
- this._clearButton = document.querySelector('#extension-hotkey-list-clear-all');
- this._listContainer = document.querySelector('#extension-hotkey-list');
+ 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();
@@ -61,10 +73,16 @@ export class ExtensionKeyboardShortcutController {
this._setupCommands(commands);
}
+ /**
+ * @param {string} name
+ * @returns {Promise<{key: ?string, modifiers: import('input').Modifier[]}>}
+ */
async resetCommand(name) {
await this._resetCommand(name);
+ /** @type {?string} */
let key = null;
+ /** @type {import('input').Modifier[]} */
let modifiers = [];
const commands = await this._getCommands();
@@ -78,32 +96,60 @@ export class ExtensionKeyboardShortcutController {
return {key, modifiers};
}
+ /**
+ * @param {string} name
+ * @param {?string} key
+ * @param {import('input').Modifier[]} modifiers
+ */
async updateCommand(name, key, modifiers) {
// Firefox-only; uses Promise API
const shortcut = this._hotkeyUtil.convertInputToCommand(key, modifiers);
- return await chrome.commands.update({name, shortcut});
+ await browser.commands.update({name, shortcut});
}
+ /**
+ * @returns {boolean}
+ */
canResetCommands() {
- return isObject(chrome.commands) && typeof chrome.commands.reset === 'function';
+ return (
+ typeof browser === 'object' && browser !== null &&
+ typeof browser.commands === 'object' && browser.commands !== null &&
+ typeof browser.commands.reset === 'function'
+ );
}
+ /**
+ * @returns {boolean}
+ */
canModifyCommands() {
- return isObject(chrome.commands) && typeof chrome.commands.update === 'function';
+ return (
+ typeof browser === 'object' && browser !== null &&
+ typeof browser.commands === 'object' && browser.commands !== null &&
+ typeof browser.commands.update === 'function'
+ );
}
// Add
+ /**
+ * @param {MouseEvent} e
+ */
_onResetClick(e) {
e.preventDefault();
this._resetAllCommands();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onClearClick(e) {
e.preventDefault();
this._clearAllCommands();
}
+ /**
+ * @returns {Promise<chrome.commands.Command[]>}
+ */
_getCommands() {
return new Promise((resolve, reject) => {
if (!(isObject(chrome.commands) && typeof chrome.commands.getAll === 'function')) {
@@ -122,6 +168,9 @@ export class ExtensionKeyboardShortcutController {
});
}
+ /**
+ * @param {chrome.commands.Command[]} commands
+ */
_setupCommands(commands) {
for (const entry of this._entries) {
entry.cleanup();
@@ -131,7 +180,7 @@ export class ExtensionKeyboardShortcutController {
const fragment = document.createDocumentFragment();
for (const {name, description, shortcut} of commands) {
- if (name.startsWith('_')) { continue; }
+ if (typeof name !== 'string' || name.startsWith('_')) { continue; }
const {key, modifiers} = this._hotkeyUtil.convertCommandToInput(shortcut);
@@ -143,10 +192,12 @@ export class ExtensionKeyboardShortcutController {
this._entries.push(entry);
}
- this._listContainer.textContent = '';
- this._listContainer.appendChild(fragment);
+ const listContainer = /** @type {HTMLElement} */ (this._listContainer);
+ listContainer.textContent = '';
+ listContainer.appendChild(fragment);
}
+ /** */
async _resetAllCommands() {
if (!this.canModifyCommands()) { return; }
@@ -154,7 +205,7 @@ export class ExtensionKeyboardShortcutController {
const promises = [];
for (const {name} of commands) {
- if (name.startsWith('_')) { continue; }
+ if (typeof name !== 'string' || name.startsWith('_')) { continue; }
promises.push(this._resetCommand(name));
}
@@ -164,6 +215,7 @@ export class ExtensionKeyboardShortcutController {
this._setupCommands(commands);
}
+ /** */
async _clearAllCommands() {
if (!this.canModifyCommands()) { return; }
@@ -171,7 +223,7 @@ export class ExtensionKeyboardShortcutController {
const promises = [];
for (const {name} of commands) {
- if (name.startsWith('_')) { continue; }
+ if (typeof name !== 'string' || name.startsWith('_')) { continue; }
promises.push(this.updateCommand(name, null, []));
}
@@ -181,31 +233,55 @@ export class ExtensionKeyboardShortcutController {
this._setupCommands(commands);
}
+ /**
+ * @param {string} name
+ */
async _resetCommand(name) {
// Firefox-only; uses Promise API
- return await chrome.commands.reset(name);
+ await browser.commands.reset(name);
}
}
class ExtensionKeyboardShortcutHotkeyEntry {
+ /**
+ * @param {ExtensionKeyboardShortcutController} parent
+ * @param {Element} node
+ * @param {string} name
+ * @param {string|undefined} description
+ * @param {?string} key
+ * @param {import('input').Modifier[]} modifiers
+ * @param {?import('environment').OperatingSystem} os
+ */
constructor(parent, node, name, description, key, modifiers, os) {
+ /** @type {ExtensionKeyboardShortcutController} */
this._parent = parent;
+ /** @type {Element} */
this._node = node;
+ /** @type {string} */
this._name = name;
+ /** @type {string|undefined} */
this._description = description;
+ /** @type {?string} */
this._key = key;
+ /** @type {import('input').Modifier[]} */
this._modifiers = modifiers;
+ /** @type {?import('environment').OperatingSystem} */
this._os = os;
+ /** @type {?HTMLInputElement} */
this._input = null;
+ /** @type {?KeyboardMouseInputField} */
this._inputField = null;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
}
+ /** */
prepare() {
- this._node.querySelector('.settings-item-label').textContent = this._description || this._name;
+ const label = /** @type {HTMLElement} */ (this._node.querySelector('.settings-item-label'));
+ label.textContent = this._description || this._name;
- const button = this._node.querySelector('.extension-hotkey-list-item-button');
- const input = this._node.querySelector('input');
+ const button = /** @type {HTMLButtonElement} */ (this._node.querySelector('.extension-hotkey-list-item-button'));
+ const input = /** @type {HTMLInputElement} */ (this._node.querySelector('input'));
this._input = input;
@@ -222,6 +298,7 @@ class ExtensionKeyboardShortcutHotkeyEntry {
}
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
if (this._node.parentNode !== null) {
@@ -235,15 +312,22 @@ class ExtensionKeyboardShortcutHotkeyEntry {
// Private
+ /**
+ * @param {import('keyboard-mouse-input-field').ChangeEvent} e
+ */
_onInputFieldChange(e) {
const {key, modifiers} = e;
this._tryUpdateInput(key, modifiers, false);
}
+ /** */
_onInputFieldBlur() {
this._updateInput();
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'clearInput':
@@ -255,11 +339,19 @@ class ExtensionKeyboardShortcutHotkeyEntry {
}
}
+ /** */
_updateInput() {
- this._inputField.setInput(this._key, this._modifiers);
- delete this._input.dataset.invalid;
+ /** @type {KeyboardMouseInputField} */ (this._inputField).setInput(this._key, this._modifiers);
+ if (this._input !== null) {
+ delete this._input.dataset.invalid;
+ }
}
+ /**
+ * @param {?string} key
+ * @param {import('input').Modifier[]} modifiers
+ * @param {boolean} updateInput
+ */
async _tryUpdateInput(key, modifiers, updateInput) {
let okay = (key === null ? (modifiers.length === 0) : (modifiers.length !== 0));
if (okay) {
@@ -273,9 +365,13 @@ class ExtensionKeyboardShortcutHotkeyEntry {
if (okay) {
this._key = key;
this._modifiers = modifiers;
- delete this._input.dataset.invalid;
+ if (this._input !== null) {
+ delete this._input.dataset.invalid;
+ }
} else {
- this._input.dataset.invalid = 'true';
+ if (this._input !== null) {
+ this._input.dataset.invalid = 'true';
+ }
}
if (updateInput) {
@@ -283,6 +379,7 @@ class ExtensionKeyboardShortcutHotkeyEntry {
}
}
+ /** */
async _resetInput() {
const {key, modifiers} = await this._parent.resetCommand(this._name);
this._key = key;
diff --git a/ext/js/pages/settings/generic-setting-controller.js b/ext/js/pages/settings/generic-setting-controller.js
index c4104874..8666614b 100644
--- a/ext/js/pages/settings/generic-setting-controller.js
+++ b/ext/js/pages/settings/generic-setting-controller.js
@@ -16,14 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {deserializeError, isObject} from '../../core.js';
+import {ExtensionError} from '../../core/extension-error.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {DOMDataBinder} from '../../dom/dom-data-binder.js';
export class GenericSettingController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('settings-modifications').OptionsScopeType} */
this._defaultScope = 'profile';
+ /** @type {DOMDataBinder<import('generic-setting-controller').ElementMetadata>} */
this._dataBinder = new DOMDataBinder({
selector: '[data-setting]',
createElementMetadata: this._createElementMetadata.bind(this),
@@ -31,7 +37,8 @@ export class GenericSettingController {
getValues: this._getValues.bind(this),
setValues: this._setValues.bind(this)
});
- this._transforms = new Map([
+ /** @type {Map<import('generic-setting-controller').TransformType, import('generic-setting-controller').TransformFunction>} */
+ this._transforms = new Map(/** @type {[key: import('generic-setting-controller').TransformType, value: import('generic-setting-controller').TransformFunction][]} */ ([
['setAttribute', this._setAttribute.bind(this)],
['setVisibility', this._setVisibility.bind(this)],
['splitTags', this._splitTags.bind(this)],
@@ -40,41 +47,49 @@ export class GenericSettingController {
['toBoolean', this._toBoolean.bind(this)],
['toString', this._toString.bind(this)],
['conditionalConvert', this._conditionalConvert.bind(this)]
- ]);
+ ]));
}
+ /** */
async prepare() {
this._dataBinder.observe(document.body);
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
}
+ /** */
async refresh() {
await this._dataBinder.refresh();
}
// Private
+ /** */
_onOptionsChanged() {
this._dataBinder.refresh();
}
+ /**
+ * @param {Element} element
+ * @returns {import('generic-setting-controller').ElementMetadata|undefined}
+ */
_createElementMetadata(element) {
- const {dataset: {setting: path, scope, transform: transformRaw}} = element;
- let transforms;
- if (typeof transformRaw === 'string') {
- transforms = JSON.parse(transformRaw);
- if (!Array.isArray(transforms)) { transforms = [transforms]; }
- } else {
- transforms = [];
- }
+ if (!(element instanceof HTMLElement)) { return void 0; }
+ const {setting: path, scope, transform: transformRaw} = element.dataset;
+ if (typeof path !== 'string') { return void 0; }
+ const scope2 = this._normalizeScope(scope);
return {
path,
- scope,
- transforms,
+ scope: scope2 !== null ? scope2 : this._defaultScope,
+ transforms: this._getTransformDataArray(transformRaw),
transformRaw
};
}
+ /**
+ * @param {import('generic-setting-controller').ElementMetadata} metadata1
+ * @param {import('generic-setting-controller').ElementMetadata} metadata2
+ * @returns {boolean}
+ */
_compareElementMetadata(metadata1, metadata2) {
return (
metadata1.path === metadata2.path &&
@@ -83,45 +98,71 @@ export class GenericSettingController {
);
}
+ /**
+ * @param {import('dom-data-binder').GetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets
+ * @returns {Promise<import('dom-data-binder').TaskResult[]>}
+ */
async _getValues(targets) {
const defaultScope = this._defaultScope;
+ /** @type {import('settings-modifications').ScopedRead[]} */
const settingsTargets = [];
for (const {metadata: {path, scope}} of targets) {
+ /** @type {import('settings-modifications').ScopedRead} */
const target = {
path,
- scope: scope || defaultScope
+ scope: typeof scope === 'string' ? scope : defaultScope,
+ optionsContext: null
};
settingsTargets.push(target);
}
return this._transformResults(await this._settingsController.getSettings(settingsTargets), targets);
}
+ /**
+ * @param {import('dom-data-binder').SetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets
+ * @returns {Promise<import('dom-data-binder').TaskResult[]>}
+ */
async _setValues(targets) {
const defaultScope = this._defaultScope;
+ /** @type {import('settings-modifications').ScopedModification[]} */
const settingsTargets = [];
for (const {metadata: {path, scope, transforms}, value, element} of targets) {
const transformedValue = this._applyTransforms(value, transforms, 'pre', element);
+ /** @type {import('settings-modifications').ScopedModification} */
const target = {
path,
- scope: scope || defaultScope,
+ scope: typeof scope === 'string' ? scope : defaultScope,
action: 'set',
- value: transformedValue
+ value: transformedValue,
+ optionsContext: null
};
settingsTargets.push(target);
}
return this._transformResults(await this._settingsController.modifySettings(settingsTargets), targets);
}
+ /**
+ * @param {import('settings-controller').ModifyResult[]} values
+ * @param {import('dom-data-binder').GetValuesDetails<import('generic-setting-controller').ElementMetadata>[]|import('dom-data-binder').SetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets
+ * @returns {import('dom-data-binder').TaskResult[]}
+ */
_transformResults(values, targets) {
return values.map((value, i) => {
const error = value.error;
- if (error) { return deserializeError(error); }
+ if (error) { return {error: ExtensionError.deserialize(error)}; }
const {metadata: {transforms}, element} = targets[i];
const result = this._applyTransforms(value.result, transforms, 'post', element);
return {result};
});
}
+ /**
+ * @param {unknown} value
+ * @param {import('generic-setting-controller').TransformData[]} transforms
+ * @param {import('generic-setting-controller').TransformStep} step
+ * @param {Element} element
+ * @returns {unknown}
+ */
_applyTransforms(value, transforms, step, element) {
for (const transform of transforms) {
const transformStep = transform.step;
@@ -135,6 +176,11 @@ export class GenericSettingController {
return value;
}
+ /**
+ * @param {?Node} node
+ * @param {number} ancestorDistance
+ * @returns {?Node}
+ */
_getAncestor(node, ancestorDistance) {
if (ancestorDistance < 0) {
return document.documentElement;
@@ -145,6 +191,12 @@ export class GenericSettingController {
return node;
}
+ /**
+ * @param {?Node} node
+ * @param {number|undefined} ancestorDistance
+ * @param {string|undefined} selector
+ * @returns {?Node}
+ */
_getRelativeElement(node, ancestorDistance, selector) {
const selectorRoot = (
typeof ancestorDistance === 'number' ?
@@ -154,12 +206,17 @@ export class GenericSettingController {
if (selectorRoot === null) { return null; }
return (
- typeof selector === 'string' ?
+ typeof selector === 'string' && (selectorRoot instanceof Element || selectorRoot instanceof Document) ?
selectorRoot.querySelector(selector) :
(selectorRoot === document ? document.documentElement : selectorRoot)
);
}
+ /**
+ * @param {import('generic-setting-controller').OperationData} operationData
+ * @param {unknown} lhs
+ * @returns {unknown}
+ */
_evaluateSimpleOperation(operationData, lhs) {
const {op: operation, value: rhs} = operationData;
switch (operation) {
@@ -167,18 +224,18 @@ export class GenericSettingController {
case '!!': return !!lhs;
case '===': return lhs === rhs;
case '!==': return lhs !== rhs;
- case '>=': return lhs >= rhs;
- case '<=': return lhs <= rhs;
- case '>': return lhs > rhs;
- case '<': return lhs < rhs;
+ case '>=': return /** @type {number} */ (lhs) >= /** @type {number} */ (rhs);
+ case '<=': return /** @type {number} */ (lhs) <= /** @type {number} */ (rhs);
+ case '>': return /** @type {number} */ (lhs) > /** @type {number} */ (rhs);
+ case '<': return /** @type {number} */ (lhs) < /** @type {number} */ (rhs);
case '&&':
- for (const operationData2 of rhs) {
+ for (const operationData2 of /** @type {import('generic-setting-controller').OperationData[]} */ (rhs)) {
const result = this._evaluateSimpleOperation(operationData2, lhs);
if (!result) { return result; }
}
return true;
case '||':
- for (const operationData2 of rhs) {
+ for (const operationData2 of /** @type {import('generic-setting-controller').OperationData[]} */ (rhs)) {
const result = this._evaluateSimpleOperation(operationData2, lhs);
if (result) { return result; }
}
@@ -188,48 +245,112 @@ export class GenericSettingController {
}
}
+ /**
+ * @param {string|undefined} value
+ * @returns {?import('settings-modifications').OptionsScopeType}
+ */
+ _normalizeScope(value) {
+ switch (value) {
+ case 'profile':
+ case 'global':
+ return value;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @param {string|undefined} transformRaw
+ * @returns {import('generic-setting-controller').TransformData[]}
+ */
+ _getTransformDataArray(transformRaw) {
+ if (typeof transformRaw === 'string') {
+ const transforms = JSON.parse(transformRaw);
+ return Array.isArray(transforms) ? transforms : [transforms];
+ }
+ return [];
+ }
+
// Transforms
+ /**
+ * @param {unknown} value
+ * @param {import('generic-setting-controller').SetAttributeTransformData} data
+ * @param {Element} element
+ * @returns {unknown}
+ */
_setAttribute(value, data, element) {
const {ancestorDistance, selector, attribute} = data;
const relativeElement = this._getRelativeElement(element, ancestorDistance, selector);
- if (relativeElement !== null) {
+ if (relativeElement !== null && relativeElement instanceof Element) {
relativeElement.setAttribute(attribute, `${value}`);
}
return value;
}
+ /**
+ * @param {unknown} value
+ * @param {import('generic-setting-controller').SetVisibilityTransformData} data
+ * @param {Element} element
+ * @returns {unknown}
+ */
_setVisibility(value, data, element) {
const {ancestorDistance, selector, condition} = data;
const relativeElement = this._getRelativeElement(element, ancestorDistance, selector);
- if (relativeElement !== null) {
+ if (relativeElement !== null && relativeElement instanceof HTMLElement) {
relativeElement.hidden = !this._evaluateSimpleOperation(condition, value);
}
return value;
}
+ /**
+ * @param {unknown} value
+ * @returns {string[]}
+ */
_splitTags(value) {
return `${value}`.split(/[,; ]+/).filter((v) => !!v);
}
+ /**
+ * @param {unknown} value
+ * @returns {string}
+ */
_joinTags(value) {
- return value.join(' ');
+ return Array.isArray(value) ? value.join(' ') : '';
}
+ /**
+ * @param {unknown} value
+ * @param {import('generic-setting-controller').ToNumberConstraintsTransformData} data
+ * @returns {number}
+ */
_toNumber(value, data) {
- let {constraints} = data;
- if (!isObject(constraints)) { constraints = {}; }
- return DocumentUtil.convertElementValueToNumber(value, constraints);
+ /** @type {import('document-util').ToNumberConstraints} */
+ const constraints = typeof data.constraints === 'object' && data.constraints !== null ? data.constraints : {};
+ return typeof value === 'string' ? DocumentUtil.convertElementValueToNumber(value, constraints) : 0;
}
+ /**
+ * @param {string} value
+ * @returns {boolean}
+ */
_toBoolean(value) {
return (value === 'true');
}
+ /**
+ * @param {unknown} value
+ * @returns {string}
+ */
_toString(value) {
return `${value}`;
}
+ /**
+ * @param {unknown} value
+ * @param {import('generic-setting-controller').ConditionalConvertTransformData} data
+ * @returns {unknown}
+ */
_conditionalConvert(value, data) {
const {cases} = data;
if (Array.isArray(cases)) {
diff --git a/ext/js/pages/settings/keyboard-mouse-input-field.js b/ext/js/pages/settings/keyboard-mouse-input-field.js
index aee01a36..f0a53f1a 100644
--- a/ext/js/pages/settings/keyboard-mouse-input-field.js
+++ b/ext/js/pages/settings/keyboard-mouse-input-field.js
@@ -20,31 +20,58 @@ import {EventDispatcher, EventListenerCollection} from '../../core.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {HotkeyUtil} from '../../input/hotkey-util.js';
+/**
+ * @augments EventDispatcher<import('keyboard-mouse-input-field').EventType>
+ */
export class KeyboardMouseInputField extends EventDispatcher {
+ /**
+ * @param {HTMLInputElement} inputNode
+ * @param {?HTMLButtonElement} mouseButton
+ * @param {?import('environment').OperatingSystem} os
+ * @param {?(pointerType: string) => boolean} [isPointerTypeSupported]
+ */
constructor(inputNode, mouseButton, os, isPointerTypeSupported=null) {
super();
+ /** @type {HTMLInputElement} */
this._inputNode = inputNode;
+ /** @type {?HTMLButtonElement} */
this._mouseButton = mouseButton;
+ /** @type {?(pointerType: string) => boolean} */
this._isPointerTypeSupported = isPointerTypeSupported;
+ /** @type {HotkeyUtil} */
this._hotkeyUtil = new HotkeyUtil(os);
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?string} */
this._key = null;
+ /** @type {import('input').Modifier[]} */
this._modifiers = [];
+ /** @type {Set<number>} */
this._penPointerIds = new Set();
+ /** @type {boolean} */
this._mouseModifiersSupported = false;
+ /** @type {boolean} */
this._keySupported = false;
}
+ /** @type {import('input').Modifier[]} */
get modifiers() {
return this._modifiers;
}
+ /**
+ * @param {?string} key
+ * @param {import('input').Modifier[]} modifiers
+ * @param {boolean} [mouseModifiersSupported]
+ * @param {boolean} [keySupported]
+ */
prepare(key, modifiers, mouseModifiersSupported=false, keySupported=false) {
this.cleanup();
this._mouseModifiersSupported = mouseModifiersSupported;
this._keySupported = keySupported;
this.setInput(key, modifiers);
+ /** @type {import('event-listener-collection').AddEventListenerArgs[]} */
const events = [
[this._inputNode, 'keydown', this._onModifierKeyDown.bind(this), false],
[this._inputNode, 'keyup', this._onModifierKeyUp.bind(this), false]
@@ -65,12 +92,17 @@ export class KeyboardMouseInputField extends EventDispatcher {
}
}
+ /**
+ * @param {?string} key
+ * @param {import('input').Modifier[]} modifiers
+ */
setInput(key, modifiers) {
this._key = key;
this._modifiers = this._sortModifiers(modifiers);
this._updateDisplayString();
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
this._modifiers = [];
@@ -80,21 +112,31 @@ export class KeyboardMouseInputField extends EventDispatcher {
this._penPointerIds.clear();
}
+ /** */
clearInputs() {
this._updateModifiers([], null);
}
// Private
+ /**
+ * @param {import('input').Modifier[]} modifiers
+ * @returns {import('input').Modifier[]}
+ */
_sortModifiers(modifiers) {
return this._hotkeyUtil.sortModifiers(modifiers);
}
+ /** */
_updateDisplayString() {
const displayValue = this._hotkeyUtil.getInputDisplayValue(this._key, this._modifiers);
this._inputNode.value = displayValue;
}
+ /**
+ * @param {KeyboardEvent} e
+ * @returns {Set<import('input').ModifierKey>}
+ */
_getModifierKeys(e) {
const modifiers = new Set(DocumentUtil.getActiveModifiers(e));
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey
@@ -115,6 +157,10 @@ export class KeyboardMouseInputField extends EventDispatcher {
return modifiers;
}
+ /**
+ * @param {string|undefined} keyName
+ * @returns {boolean}
+ */
_isModifierKey(keyName) {
switch (keyName) {
case 'AltLeft':
@@ -133,9 +179,13 @@ export class KeyboardMouseInputField extends EventDispatcher {
}
}
+ /**
+ * @param {KeyboardEvent} e
+ */
_onModifierKeyDown(e) {
e.preventDefault();
+ /** @type {string|undefined} */
let key = e.code;
if (key === 'Unidentified' || key === '') { key = void 0; }
if (this._keySupported) {
@@ -153,15 +203,24 @@ export class KeyboardMouseInputField extends EventDispatcher {
}
}
+ /**
+ * @param {KeyboardEvent} e
+ */
_onModifierKeyUp(e) {
e.preventDefault();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onMouseButtonMouseDown(e) {
e.preventDefault();
this._addModifiers(DocumentUtil.getActiveButtons(e));
}
+ /**
+ * @param {PointerEvent} e
+ */
_onMouseButtonPointerDown(e) {
if (!e.isPrimary) { return; }
@@ -179,6 +238,9 @@ export class KeyboardMouseInputField extends EventDispatcher {
this._addModifiers(DocumentUtil.getActiveButtons(e));
}
+ /**
+ * @param {PointerEvent} e
+ */
_onMouseButtonPointerOver(e) {
const {pointerType, pointerId} = e;
if (pointerType === 'pen') {
@@ -186,23 +248,39 @@ export class KeyboardMouseInputField extends EventDispatcher {
}
}
+ /**
+ * @param {PointerEvent} e
+ */
_onMouseButtonPointerOut(e) {
const {pointerId} = e;
this._penPointerIds.delete(pointerId);
}
+ /**
+ * @param {PointerEvent} e
+ */
_onMouseButtonPointerCancel(e) {
this._onMouseButtonPointerOut(e);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onMouseButtonMouseUp(e) {
e.preventDefault();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onMouseButtonContextMenu(e) {
e.preventDefault();
}
+ /**
+ * @param {Iterable<import('input').Modifier>} newModifiers
+ * @param {?string} [newKey]
+ */
_addModifiers(newModifiers, newKey) {
const modifiers = new Set(this._modifiers);
for (const modifier of newModifiers) {
@@ -211,6 +289,10 @@ export class KeyboardMouseInputField extends EventDispatcher {
this._updateModifiers([...modifiers], newKey);
}
+ /**
+ * @param {import('input').Modifier[]} modifiers
+ * @param {?string} [newKey]
+ */
_updateModifiers(modifiers, newKey) {
modifiers = this._sortModifiers(modifiers);
@@ -226,10 +308,18 @@ export class KeyboardMouseInputField extends EventDispatcher {
this._updateDisplayString();
if (changed) {
- this.trigger('change', {modifiers: this._modifiers, key: this._key});
+ /** @type {import('keyboard-mouse-input-field').ChangeEvent} */
+ const event = {modifiers: this._modifiers, key: this._key};
+ this.trigger('change', event);
}
}
+ /**
+ * @template T
+ * @param {T[]} array1
+ * @param {T[]} array2
+ * @returns {boolean}
+ */
_areArraysEqual(array1, array2) {
const length = array1.length;
if (length !== array2.length) { return false; }
diff --git a/ext/js/pages/settings/keyboard-shortcuts-controller.js b/ext/js/pages/settings/keyboard-shortcuts-controller.js
index e7ad4d15..ad16b0e9 100644
--- a/ext/js/pages/settings/keyboard-shortcuts-controller.js
+++ b/ext/js/pages/settings/keyboard-shortcuts-controller.js
@@ -23,16 +23,29 @@ import {yomitan} from '../../yomitan.js';
import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';
export class KeyboardShortcutController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {KeyboardShortcutHotkeyEntry[]} */
this._entries = [];
+ /** @type {?import('environment').OperatingSystem} */
this._os = null;
+ /** @type {?HTMLButtonElement} */
this._addButton = null;
+ /** @type {?HTMLButtonElement} */
this._resetButton = null;
+ /** @type {?HTMLElement} */
this._listContainer = null;
+ /** @type {?HTMLElement} */
this._emptyIndicator = null;
+ /** @type {Intl.Collator} */
this._stringComparer = new Intl.Collator('en-US'); // Invariant locale
+ /** @type {?HTMLElement} */
this._scrollContainer = null;
+ /** @type {Map<string, import('keyboard-shortcut-controller').ActionDetails>} */
this._actionDetails = new Map([
['', {scopes: new Set()}],
['close', {scopes: new Set(['popup', 'search'])}],
@@ -58,19 +71,21 @@ export class KeyboardShortcutController {
]);
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._settingsController;
}
+ /** */
async prepare() {
const {platform: {os}} = await yomitan.api.getEnvironmentInfo();
this._os = os;
- this._addButton = document.querySelector('#hotkey-list-add');
- this._resetButton = document.querySelector('#hotkey-list-reset');
- this._listContainer = document.querySelector('#hotkey-list');
- this._emptyIndicator = document.querySelector('#hotkey-list-empty');
- this._scrollContainer = document.querySelector('#keyboard-shortcuts-modal .modal-body');
+ this._addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#hotkey-list-add'));
+ this._resetButton = /** @type {HTMLButtonElement} */ (document.querySelector('#hotkey-list-reset'));
+ this._listContainer = /** @type {HTMLElement} */ (document.querySelector('#hotkey-list'));
+ this._emptyIndicator = /** @type {HTMLElement} */ (document.querySelector('#hotkey-list-empty'));
+ this._scrollContainer = /** @type {HTMLElement} */ (document.querySelector('#keyboard-shortcuts-modal .modal-body'));
this._addButton.addEventListener('click', this._onAddClick.bind(this));
this._resetButton.addEventListener('click', this._onResetClick.bind(this));
@@ -79,6 +94,9 @@ export class KeyboardShortcutController {
await this._updateOptions();
}
+ /**
+ * @param {import('settings').InputsHotkeyOptions} terminationCharacterEntry
+ */
async addEntry(terminationCharacterEntry) {
const options = await this._settingsController.getOptions();
const {inputs: {hotkeys}} = options;
@@ -92,9 +110,14 @@ export class KeyboardShortcutController {
}]);
await this._updateOptions();
- this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
+ const scrollContainer = /** @type {HTMLElement} */ (this._scrollContainer);
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
+ /**
+ * @param {number} index
+ * @returns {Promise<boolean>}
+ */
async deleteEntry(index) {
const options = await this._settingsController.getOptions();
const {inputs: {hotkeys}} = options;
@@ -113,55 +136,79 @@ export class KeyboardShortcutController {
return true;
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async modifyProfileSettings(targets) {
return await this._settingsController.modifyProfileSettings(targets);
}
+ /**
+ * @returns {Promise<import('settings').InputsHotkeyOptions[]>}
+ */
async getDefaultHotkeys() {
const defaultOptions = await this._settingsController.getDefaultOptions();
return defaultOptions.profiles[0].options.inputs.hotkeys;
}
+ /**
+ * @param {string} action
+ * @returns {import('keyboard-shortcut-controller').ActionDetails|undefined}
+ */
getActionDetails(action) {
return this._actionDetails.get(action);
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
for (const entry of this._entries) {
entry.cleanup();
}
this._entries = [];
+ const os = /** @type {import('environment').OperatingSystem} */ (this._os);
const {inputs: {hotkeys}} = options;
const fragment = document.createDocumentFragment();
for (let i = 0, ii = hotkeys.length; i < ii; ++i) {
const hotkeyEntry = hotkeys[i];
- const node = this._settingsController.instantiateTemplate('hotkey-list-item');
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('hotkey-list-item'));
fragment.appendChild(node);
- const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, this._os, this._stringComparer);
+ const entry = new KeyboardShortcutHotkeyEntry(this, hotkeyEntry, i, node, os, this._stringComparer);
this._entries.push(entry);
entry.prepare();
}
- this._listContainer.appendChild(fragment);
- this._listContainer.hidden = (hotkeys.length === 0);
- this._emptyIndicator.hidden = (hotkeys.length !== 0);
+ const listContainer = /** @type {HTMLElement} */ (this._listContainer);
+ listContainer.appendChild(fragment);
+ listContainer.hidden = (hotkeys.length === 0);
+ /** @type {HTMLElement} */ (this._emptyIndicator).hidden = (hotkeys.length !== 0);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onAddClick(e) {
e.preventDefault();
this._addNewEntry();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onResetClick(e) {
e.preventDefault();
this._reset();
}
+ /** */
async _addNewEntry() {
+ /** @type {import('settings').InputsHotkeyOptions} */
const newEntry = {
action: '',
argument: '',
@@ -170,14 +217,17 @@ export class KeyboardShortcutController {
scopes: ['popup', 'search'],
enabled: true
};
- return await this.addEntry(newEntry);
+ await this.addEntry(newEntry);
}
+ /** */
async _updateOptions() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /** */
async _reset() {
const value = await this.getDefaultHotkeys();
await this._settingsController.setProfileSetting('inputs.hotkeys', value);
@@ -186,34 +236,59 @@ export class KeyboardShortcutController {
}
class KeyboardShortcutHotkeyEntry {
+ /**
+ * @param {KeyboardShortcutController} parent
+ * @param {import('settings').InputsHotkeyOptions} data
+ * @param {number} index
+ * @param {HTMLElement} node
+ * @param {import('environment').OperatingSystem} os
+ * @param {Intl.Collator} stringComparer
+ */
constructor(parent, data, index, node, os, stringComparer) {
+ /** @type {KeyboardShortcutController} */
this._parent = parent;
+ /** @type {import('settings').InputsHotkeyOptions} */
this._data = data;
+ /** @type {number} */
this._index = index;
+ /** @type {HTMLElement} */
this._node = node;
+ /** @type {import('environment').OperatingSystem} */
this._os = os;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?KeyboardMouseInputField} */
this._inputField = null;
+ /** @type {?HTMLSelectElement} */
this._actionSelect = null;
+ /** @type {string} */
this._basePath = `inputs.hotkeys[${this._index}]`;
+ /** @type {Intl.Collator} */
this._stringComparer = stringComparer;
+ /** @type {?HTMLButtonElement} */
this._enabledButton = null;
+ /** @type {?import('../../dom/popup-menu.js').PopupMenu} */
this._scopeMenu = null;
+ /** @type {EventListenerCollection} */
this._scopeMenuEventListeners = new EventListenerCollection();
+ /** @type {?HTMLElement} */
this._argumentContainer = null;
+ /** @type {?HTMLInputElement} */
this._argumentInput = null;
+ /** @type {EventListenerCollection} */
this._argumentEventListeners = new EventListenerCollection();
}
+ /** */
prepare() {
const node = this._node;
- const menuButton = node.querySelector('.hotkey-list-item-button');
- const input = node.querySelector('.hotkey-list-item-input');
- const action = node.querySelector('.hotkey-list-item-action');
- const enabledToggle = node.querySelector('.hotkey-list-item-enabled');
- const scopesButton = node.querySelector('.hotkey-list-item-scopes-button');
- const enabledButton = node.querySelector('.hotkey-list-item-enabled-button');
+ const menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-button'));
+ const input = /** @type {HTMLInputElement} */ (node.querySelector('.hotkey-list-item-input'));
+ const action = /** @type {HTMLSelectElement} */ (node.querySelector('.hotkey-list-item-action'));
+ const enabledToggle = /** @type {HTMLInputElement} */ (node.querySelector('.hotkey-list-item-enabled'));
+ const scopesButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-scopes-button'));
+ const enabledButton = /** @type {HTMLButtonElement} */ (node.querySelector('.hotkey-list-item-enabled-button'));
this._actionSelect = action;
this._enabledButton = enabledButton;
@@ -238,9 +313,10 @@ class KeyboardShortcutHotkeyEntry {
this._eventListeners.on(this._inputField, 'change', this._onInputFieldChange.bind(this));
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
- this._inputField.cleanup();
+ /** @type {KeyboardMouseInputField} */ (this._inputField).cleanup();
this._clearScopeMenu();
this._clearArgumentEventListeners();
if (this._node.parentNode !== null) {
@@ -250,11 +326,14 @@ class KeyboardShortcutHotkeyEntry {
// Private
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const {action} = this._data;
const {menu} = e.detail;
- const resetArgument = menu.bodyNode.querySelector('.popup-menu-item[data-menu-action="resetArgument"]');
+ const resetArgument = /** @type {HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action="resetArgument"]'));
const details = this._parent.getActionDetails(action);
const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0;
@@ -262,13 +341,16 @@ class KeyboardShortcutHotkeyEntry {
resetArgument.hidden = (typeof argumentDetails === 'undefined');
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'delete':
this._delete();
break;
case 'clearInputs':
- this._inputField.clearInputs();
+ /** @type {KeyboardMouseInputField} */ (this._inputField).clearInputs();
break;
case 'resetInput':
this._resetInput();
@@ -279,10 +361,13 @@ class KeyboardShortcutHotkeyEntry {
}
}
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onScopesMenuOpen(e) {
const {menu} = e.detail;
const validScopes = this._getValidScopesForAction(this._data.action);
- if (validScopes.size === 0) {
+ if (validScopes === null || validScopes.size === 0) {
menu.close();
return;
}
@@ -291,6 +376,9 @@ class KeyboardShortcutHotkeyEntry {
this._updateDisplay(menu.containerNode); // Fix a animation issue due to changing checkbox values
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onScopesMenuClose(e) {
const {menu, action} = e.detail;
if (action === 'toggleScope') {
@@ -302,24 +390,45 @@ class KeyboardShortcutHotkeyEntry {
}
}
+ /**
+ * @param {import('keyboard-mouse-input-field').ChangeEvent} details
+ */
_onInputFieldChange({key, modifiers}) {
- this._setKeyAndModifiers(key, modifiers);
+ /** @type {import('input').ModifierKey[]} */
+ const modifiers2 = [];
+ for (const modifier of modifiers) {
+ const modifier2 = DocumentUtil.normalizeModifierKey(modifier);
+ if (modifier2 === null) { continue; }
+ modifiers2.push(modifier2);
+ }
+ this._setKeyAndModifiers(key, modifiers2);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onScopeCheckboxChange(e) {
- const node = e.currentTarget;
- const {scope} = node.dataset;
- if (typeof scope !== 'string') { return; }
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const scope = this._normalizeScope(node.dataset.scope);
+ if (scope === null) { return; }
this._setScopeEnabled(scope, node.checked);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onActionSelectChange(e) {
- const value = e.currentTarget.value;
+ const node = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const value = node.value;
this._setAction(value);
}
+ /**
+ * @param {string} template
+ * @param {Event} e
+ */
_onArgumentValueChange(template, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
let value = this._getArgumentInputValue(node);
switch (template) {
case 'hotkey-argument-move-offset':
@@ -329,10 +438,15 @@ class KeyboardShortcutHotkeyEntry {
this._setArgument(value);
}
+ /** */
async _delete() {
this._parent.deleteEntry(this._index);
}
+ /**
+ * @param {?string} key
+ * @param {import('input').ModifierKey[]} modifiers
+ */
async _setKeyAndModifiers(key, modifiers) {
this._data.key = key;
this._data.modifiers = modifiers;
@@ -350,6 +464,10 @@ class KeyboardShortcutHotkeyEntry {
]);
}
+ /**
+ * @param {import('settings').InputsHotkeyScope} scope
+ * @param {boolean} enabled
+ */
async _setScopeEnabled(scope, enabled) {
const scopes = this._data.scopes;
const index = scopes.indexOf(scope);
@@ -372,10 +490,15 @@ class KeyboardShortcutHotkeyEntry {
this._updateScopesButton();
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async _modifyProfileSettings(targets) {
return await this._parent.settingsController.modifyProfileSettings(targets);
}
+ /** */
async _resetInput() {
const defaultHotkeys = await this._parent.getDefaultHotkeys();
const defaultValue = this._getDefaultKeyAndModifiers(defaultHotkeys, this._data.action);
@@ -383,9 +506,10 @@ class KeyboardShortcutHotkeyEntry {
const {key, modifiers} = defaultValue;
await this._setKeyAndModifiers(key, modifiers);
- this._inputField.setInput(key, modifiers);
+ /** @type {KeyboardMouseInputField} */ (this._inputField).setInput(key, modifiers);
}
+ /** */
async _resetArgument() {
const {action} = this._data;
const details = this._parent.getActionDetails(action);
@@ -395,6 +519,11 @@ class KeyboardShortcutHotkeyEntry {
await this._setArgument(argumentDefault);
}
+ /**
+ * @param {import('settings').InputsHotkeyOptions[]} defaultHotkeys
+ * @param {string} action
+ * @returns {?{modifiers: import('settings').InputsHotkeyModifier[], key: ?string}}
+ */
_getDefaultKeyAndModifiers(defaultHotkeys, action) {
for (const {action: action2, key, modifiers} of defaultHotkeys) {
if (action2 !== action) { continue; }
@@ -403,16 +532,18 @@ class KeyboardShortcutHotkeyEntry {
return null;
}
+ /**
+ * @param {string} value
+ */
async _setAction(value) {
const validScopesOld = this._getValidScopesForAction(this._data.action);
const scopes = this._data.scopes;
let details = this._parent.getActionDetails(value);
- if (typeof details === 'undefined') { details = {}; }
+ if (typeof details === 'undefined') { details = {scopes: new Set()}; }
- let validScopes = details.scopes;
- if (typeof validScopes === 'undefined') { validScopes = new Set(); }
+ const validScopes = details.scopes;
const {argument: argumentDetails} = details;
let defaultArgument = typeof argumentDetails !== 'undefined' ? argumentDetails.default : '';
@@ -462,6 +593,9 @@ class KeyboardShortcutHotkeyEntry {
this._updateActionArgument();
}
+ /**
+ * @param {string} value
+ */
async _setArgument(value) {
this._data.argument = value;
@@ -479,16 +613,24 @@ class KeyboardShortcutHotkeyEntry {
}]);
}
+ /** */
_updateScopesMenu() {
if (this._scopeMenu === null) { return; }
this._updateScopeMenuItems(this._scopeMenu);
}
+ /**
+ * @param {string} action
+ * @returns {?Set<import('settings').InputsHotkeyScope>}
+ */
_getValidScopesForAction(action) {
const details = this._parent.getActionDetails(action);
return typeof details !== 'undefined' ? details.scopes : null;
}
+ /**
+ * @param {import('../../dom/popup-menu.js').PopupMenu} menu
+ */
_updateScopeMenuItems(menu) {
this._scopeMenuEventListeners.removeAllEventListeners();
@@ -496,14 +638,15 @@ class KeyboardShortcutHotkeyEntry {
const validScopes = this._getValidScopesForAction(this._data.action);
const bodyNode = menu.bodyNode;
- const menuItems = bodyNode.querySelectorAll('.popup-menu-item');
+ const menuItems = /** @type {NodeListOf<HTMLElement>} */ (bodyNode.querySelectorAll('.popup-menu-item'));
for (const menuItem of menuItems) {
if (menuItem.dataset.menuAction !== 'toggleScope') { continue; }
- const {scope} = menuItem.dataset;
+ const scope = this._normalizeScope(menuItem.dataset.scope);
+ if (scope === null) { continue; }
menuItem.hidden = !(validScopes === null || validScopes.has(scope));
- const checkbox = menuItem.querySelector('.hotkey-scope-checkbox');
+ const checkbox = /** @type {HTMLInputElement} */ (menuItem.querySelector('.hotkey-scope-checkbox'));
if (checkbox !== null) {
checkbox.checked = scopes.includes(scope);
this._scopeMenuEventListeners.addEventListener(checkbox, 'change', this._onScopeCheckboxChange.bind(this), false);
@@ -511,16 +654,23 @@ class KeyboardShortcutHotkeyEntry {
}
}
+ /** */
_clearScopeMenu() {
this._scopeMenuEventListeners.removeAllEventListeners();
this._scopeMenu = null;
}
+ /** */
_updateScopesButton() {
const {scopes} = this._data;
- this._enabledButton.dataset.scopeCount = `${scopes.length}`;
+ if (this._enabledButton !== null) {
+ this._enabledButton.dataset.scopeCount = `${scopes.length}`;
+ }
}
+ /**
+ * @param {HTMLElement} node
+ */
_updateDisplay(node) {
const {style} = node;
const {display} = style;
@@ -529,49 +679,64 @@ class KeyboardShortcutHotkeyEntry {
style.display = display;
}
+ /** */
_updateActionArgument() {
this._clearArgumentEventListeners();
const {action, argument} = this._data;
const details = this._parent.getActionDetails(action);
- const {argument: argumentDetails} = typeof details !== 'undefined' ? details : {};
+ const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0;
- this._argumentContainer.textContent = '';
+ if (this._argumentContainer !== null) {
+ this._argumentContainer.textContent = '';
+ }
if (typeof argumentDetails !== 'undefined') {
const {template} = argumentDetails;
const node = this._parent.settingsController.instantiateTemplate(template);
const inputSelector = '.hotkey-argument-input';
- const inputNode = node.matches(inputSelector) ? node : node.querySelector(inputSelector);
+ const inputNode = /** @type {HTMLInputElement} */ (node.matches(inputSelector) ? node : node.querySelector(inputSelector));
if (inputNode !== null) {
this._setArgumentInputValue(inputNode, argument);
this._argumentInput = inputNode;
this._updateArgumentInputValidity();
this._argumentEventListeners.addEventListener(inputNode, 'change', this._onArgumentValueChange.bind(this, template), false);
}
- this._argumentContainer.appendChild(node);
+ if (this._argumentContainer !== null) {
+ this._argumentContainer.appendChild(node);
+ }
}
}
+ /** */
_clearArgumentEventListeners() {
this._argumentEventListeners.removeAllEventListeners();
this._argumentInput = null;
}
+ /**
+ * @param {HTMLInputElement} node
+ * @returns {string}
+ */
_getArgumentInputValue(node) {
return node.value;
}
+ /**
+ * @param {HTMLInputElement} node
+ * @param {string} value
+ */
_setArgumentInputValue(node, value) {
node.value = value;
}
+ /** */
async _updateArgumentInputValidity() {
if (this._argumentInput === null) { return; }
let okay = true;
const {action, argument} = this._data;
const details = this._parent.getActionDetails(action);
- const {argument: argumentDetails} = typeof details !== 'undefined' ? details : {};
+ const argumentDetails = typeof details !== 'undefined' ? details.argument : void 0;
if (typeof argumentDetails !== 'undefined') {
const {template} = argumentDetails;
@@ -585,6 +750,10 @@ class KeyboardShortcutHotkeyEntry {
this._argumentInput.dataset.invalid = `${!okay}`;
}
+ /**
+ * @param {string} path
+ * @returns {Promise<boolean>}
+ */
async _isHotkeyArgumentSettingPathValid(path) {
if (path.length === 0) { return true; }
@@ -601,4 +770,19 @@ class KeyboardShortcutHotkeyEntry {
}
return false;
}
+
+ /**
+ * @param {string|undefined} value
+ * @returns {?import('settings').InputsHotkeyScope}
+ */
+ _normalizeScope(value) {
+ switch (value) {
+ case 'popup':
+ case 'search':
+ case 'web':
+ return value;
+ default:
+ return null;
+ }
+ }
}
diff --git a/ext/js/pages/settings/mecab-controller.js b/ext/js/pages/settings/mecab-controller.js
index a839fc21..4e2b02c6 100644
--- a/ext/js/pages/settings/mecab-controller.js
+++ b/ext/js/pages/settings/mecab-controller.js
@@ -19,48 +19,61 @@
import {yomitan} from '../../yomitan.js';
export class MecabController {
- constructor(settingsController) {
- this._settingsController = settingsController;
+ constructor() {
+ /** @type {?HTMLButtonElement} */
this._testButton = null;
+ /** @type {?HTMLElement} */
this._resultsContainer = null;
+ /** @type {boolean} */
this._testActive = false;
}
+ /** */
prepare() {
- this._testButton = document.querySelector('#test-mecab-button');
- this._resultsContainer = document.querySelector('#test-mecab-results');
+ 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);
}
// Private
+ /**
+ * @param {MouseEvent} e
+ */
_onTestButtonClick(e) {
e.preventDefault();
this._testMecab();
}
+ /** */
async _testMecab() {
if (this._testActive) { return; }
try {
this._testActive = true;
- this._testButton.disabled = true;
- this._resultsContainer.textContent = '';
- this._resultsContainer.hidden = true;
+ const resultsContainer = /** @type {HTMLElement} */ (this._resultsContainer);
+ /** @type {HTMLButtonElement} */ (this._testButton).disabled = true;
+ resultsContainer.textContent = '';
+ resultsContainer.hidden = true;
await yomitan.api.testMecab();
this._setStatus('Connection was successful', false);
} catch (e) {
- this._setStatus(e.message, true);
+ this._setStatus(e instanceof Error ? e.message : `${e}`, true);
} finally {
this._testActive = false;
- this._testButton.disabled = false;
+ /** @type {HTMLButtonElement} */ (this._testButton).disabled = false;
}
}
+ /**
+ * @param {string} message
+ * @param {boolean} isError
+ */
_setStatus(message, isError) {
- this._resultsContainer.textContent = message;
- this._resultsContainer.hidden = false;
- this._resultsContainer.classList.toggle('danger-text', isError);
+ const resultsContainer = /** @type {HTMLElement} */ (this._resultsContainer);
+ resultsContainer.textContent = message;
+ resultsContainer.hidden = false;
+ resultsContainer.classList.toggle('danger-text', isError);
}
}
diff --git a/ext/js/pages/settings/modal-controller.js b/ext/js/pages/settings/modal-controller.js
index 517a19b3..852bdcc5 100644
--- a/ext/js/pages/settings/modal-controller.js
+++ b/ext/js/pages/settings/modal-controller.js
@@ -20,13 +20,16 @@ import {Modal} from './modal.js';
export class ModalController {
constructor() {
+ /** @type {Modal[]} */
this._modals = [];
+ /** @type {Map<string|Element, Modal>} */
this._modalMap = new Map();
}
+ /** */
prepare() {
const idSuffix = '-modal';
- for (const node of document.querySelectorAll('.modal')) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.modal'))) {
let {id} = node;
if (typeof id !== 'string') { continue; }
@@ -42,11 +45,18 @@ export class ModalController {
}
}
+ /**
+ * @param {string|Element} nameOrNode
+ * @returns {?Modal}
+ */
getModal(nameOrNode) {
const modal = this._modalMap.get(nameOrNode);
return (typeof modal !== 'undefined' ? modal : null);
}
+ /**
+ * @returns {?Modal}
+ */
getTopVisibleModal() {
for (let i = this._modals.length - 1; i >= 0; --i) {
const modal = this._modals[i];
diff --git a/ext/js/pages/settings/modal.js b/ext/js/pages/settings/modal.js
index 4d1c098d..21a6e705 100644
--- a/ext/js/pages/settings/modal.js
+++ b/ext/js/pages/settings/modal.js
@@ -19,40 +19,55 @@
import {PanelElement} from '../../dom/panel-element.js';
export class Modal extends PanelElement {
+ /**
+ * @param {HTMLElement} node
+ */
constructor(node) {
super({
node,
closingAnimationDuration: 375 // Milliseconds; includes buffer
});
+ /** @type {?Element} */
this._contentNode = null;
+ /** @type {boolean} */
this._canCloseOnClick = false;
}
+ /** */
prepare() {
const node = this.node;
this._contentNode = node.querySelector('.modal-content');
- let dimmerNode = node.querySelector('.modal-content-dimmer');
+ let dimmerNode = /** @type {?HTMLElement} */ (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);
dimmerNode.addEventListener('click', this._onModalContainerClick.bind(this), false);
- for (const actionNode of node.querySelectorAll('[data-modal-action]')) {
+ for (const actionNode of /** @type {NodeListOf<HTMLElement>} */ (node.querySelectorAll('[data-modal-action]'))) {
actionNode.addEventListener('click', this._onActionNodeClick.bind(this), false);
}
}
// Private
+ /**
+ * @param {MouseEvent} e
+ */
_onModalContainerMouseDown(e) {
this._canCloseOnClick = (e.currentTarget === e.target);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onModalContainerMouseUp(e) {
if (!this._canCloseOnClick) { return; }
this._canCloseOnClick = (e.currentTarget === e.target);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onModalContainerClick(e) {
if (!this._canCloseOnClick) { return; }
this._canCloseOnClick = false;
@@ -60,8 +75,12 @@ export class Modal extends PanelElement {
this.setVisible(false);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onActionNodeClick(e) {
- const {modalAction} = e.currentTarget.dataset;
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
+ const {modalAction} = element.dataset;
switch (modalAction) {
case 'expand':
this._setExpanded(true);
@@ -72,6 +91,9 @@ export class Modal extends PanelElement {
}
}
+ /**
+ * @param {boolean} expanded
+ */
_setExpanded(expanded) {
if (this._contentNode === null) { return; }
this._contentNode.classList.toggle('modal-content-full', expanded);
diff --git a/ext/js/pages/settings/nested-popups-controller.js b/ext/js/pages/settings/nested-popups-controller.js
index b9621ef0..c01986ab 100644
--- a/ext/js/pages/settings/nested-popups-controller.js
+++ b/ext/js/pages/settings/nested-popups-controller.js
@@ -19,50 +19,79 @@
import {DocumentUtil} from '../../dom/document-util.js';
export class NestedPopupsController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {number} */
this._popupNestingMaxDepth = 0;
+ /** @type {?HTMLInputElement} */
+ this._nestedPopupsEnabled = null;
+ /** @type {?HTMLInputElement} */
+ this._nestedPopupsCount = null;
+ /** @type {?HTMLElement} */
+ this._nestedPopupsEnabledMoreOptions = null;
}
+ /** */
async prepare() {
- this._nestedPopupsEnabled = document.querySelector('#nested-popups-enabled');
- this._nestedPopupsCount = document.querySelector('#nested-popups-count');
- this._nestedPopupsEnabledMoreOptions = document.querySelector('#nested-popups-enabled-more-options');
+ 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();
this._nestedPopupsEnabled.addEventListener('change', this._onNestedPopupsEnabledChange.bind(this), false);
this._nestedPopupsCount.addEventListener('change', this._onNestedPopupsCountChange.bind(this), false);
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
- this._onOptionsChanged({options});
+ this._onOptionsChanged({options, optionsContext});
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
this._updatePopupNestingMaxDepth(options.scanning.popupNestingMaxDepth);
}
+ /**
+ * @param {Event} e
+ */
_onNestedPopupsEnabledChange(e) {
- const value = e.currentTarget.checked;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const value = node.checked;
if (value && this._popupNestingMaxDepth > 0) { return; }
this._setPopupNestingMaxDepth(value ? 1 : 0);
}
+ /**
+ * @param {Event} e
+ */
_onNestedPopupsCountChange(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
const value = Math.max(1, DocumentUtil.convertElementValueToNumber(node.value, node));
this._setPopupNestingMaxDepth(value);
}
+ /**
+ * @param {number} value
+ */
_updatePopupNestingMaxDepth(value) {
const enabled = (value > 0);
this._popupNestingMaxDepth = value;
- this._nestedPopupsEnabled.checked = enabled;
- this._nestedPopupsCount.value = `${value}`;
- this._nestedPopupsEnabledMoreOptions.hidden = !enabled;
+ /** @type {HTMLInputElement} */ (this._nestedPopupsEnabled).checked = enabled;
+ /** @type {HTMLInputElement} */ (this._nestedPopupsCount).value = `${value}`;
+ /** @type {HTMLElement} */ (this._nestedPopupsEnabledMoreOptions).hidden = !enabled;
}
+ /**
+ * @param {number} value
+ */
async _setPopupNestingMaxDepth(value) {
this._updatePopupNestingMaxDepth(value);
await this._settingsController.setProfileSetting('scanning.popupNestingMaxDepth', value);
diff --git a/ext/js/pages/settings/permissions-origin-controller.js b/ext/js/pages/settings/permissions-origin-controller.js
index d234faa0..a4271f92 100644
--- a/ext/js/pages/settings/permissions-origin-controller.js
+++ b/ext/js/pages/settings/permissions-origin-controller.js
@@ -19,24 +19,36 @@
import {EventListenerCollection} from '../../core.js';
export class PermissionsOriginController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?HTMLElement} */
this._originContainer = null;
+ /** @type {?HTMLElement} */
this._originEmpty = null;
+ /** @type {?NodeListOf<HTMLInputElement>} */
this._originToggleNodes = null;
+ /** @type {?HTMLInputElement} */
this._addOriginInput = null;
+ /** @type {?HTMLElement} */
this._errorContainer = null;
+ /** @type {ChildNode[]} */
this._originContainerChildren = [];
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
}
+ /** */
async prepare() {
- this._originContainer = document.querySelector('#permissions-origin-list');
- this._originEmpty = document.querySelector('#permissions-origin-list-empty');
- this._originToggleNodes = document.querySelectorAll('.permissions-origin-toggle');
- this._addOriginInput = document.querySelector('#permissions-origin-new-input');
- this._errorContainer = document.querySelector('#permissions-origin-list-error');
- const addButton = document.querySelector('#permissions-origin-add');
+ this._originContainer = /** @type {HTMLElement} */ (document.querySelector('#permissions-origin-list'));
+ this._originEmpty = /** @type {HTMLElement} */ (document.querySelector('#permissions-origin-list-empty'));
+ this._originToggleNodes = /** @type {NodeListOf<HTMLInputElement>} */ (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'));
for (const node of this._originToggleNodes) {
node.addEventListener('change', this._onOriginToggleChange.bind(this), false);
@@ -49,6 +61,9 @@ export class PermissionsOriginController {
// Private
+ /**
+ * @param {import('settings-controller').PermissionsChangedEvent} details
+ */
_onPermissionsChanged({permissions}) {
this._eventListeners.removeAllEventListeners();
for (const node of this._originContainerChildren) {
@@ -57,9 +72,11 @@ export class PermissionsOriginController {
}
this._originContainerChildren = [];
+ /** @type {Set<string>} */
const originsSet = new Set(permissions.origins);
- for (const node of this._originToggleNodes) {
- node.checked = originsSet.has(node.dataset.origin);
+ for (const node of /** @type {NodeListOf<HTMLInputElement>} */ (this._originToggleNodes)) {
+ const {origin} = node.dataset;
+ node.checked = typeof origin === 'string' && originsSet.has(origin);
}
let any = false;
@@ -67,60 +84,78 @@ export class PermissionsOriginController {
'<all_urls>'
]);
const fragment = document.createDocumentFragment();
- for (const origin of permissions.origins) {
+ for (const origin of originsSet) {
if (excludeOrigins.has(origin)) { continue; }
const node = this._settingsController.instantiateTemplateFragment('permissions-origin');
- const input = node.querySelector('.permissions-origin-input');
- const menuButton = node.querySelector('.permissions-origin-button');
+ const input = /** @type {HTMLInputElement} */ (node.querySelector('.permissions-origin-input'));
+ const menuButton = /** @type {HTMLElement} */ (node.querySelector('.permissions-origin-button'));
input.value = origin;
this._eventListeners.addEventListener(menuButton, 'menuClose', this._onOriginMenuClose.bind(this, origin), false);
this._originContainerChildren.push(...node.childNodes);
fragment.appendChild(node);
any = true;
}
- this._originContainer.insertBefore(fragment, this._originContainer.firstChild);
- this._originEmpty.hidden = any;
+ const container = /** @type {HTMLElement} */ (this._originContainer);
+ container.insertBefore(fragment, container.firstChild);
+ /** @type {HTMLElement} */ (this._originEmpty).hidden = any;
- this._errorContainer.hidden = true;
+ /** @type {HTMLElement} */ (this._errorContainer).hidden = true;
}
+ /**
+ * @param {Event} e
+ */
_onOriginToggleChange(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
const value = node.checked;
node.checked = !value;
const {origin} = node.dataset;
+ if (typeof origin !== 'string') { return; }
this._setOriginPermissionEnabled(origin, value);
}
+ /**
+ * @param {string} origin
+ */
_onOriginMenuClose(origin) {
this._setOriginPermissionEnabled(origin, false);
}
+ /** */
_onAddButtonClick() {
this._addOrigin();
}
+ /** */
async _addOrigin() {
- const origin = this._addOriginInput.value;
+ const input = /** @type {HTMLInputElement} */ (this._addOriginInput);
+ const origin = input.value;
const added = await this._setOriginPermissionEnabled(origin, true);
if (added) {
- this._addOriginInput.value = '';
+ input.value = '';
}
}
+ /** */
async _updatePermissions() {
const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
this._onPermissionsChanged({permissions});
}
+ /**
+ * @param {string} origin
+ * @param {boolean} enabled
+ * @returns {Promise<boolean>}
+ */
async _setOriginPermissionEnabled(origin, enabled) {
let added = false;
try {
added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled);
} catch (e) {
- this._errorContainer.hidden = false;
- this._errorContainer.textContent = e.message;
+ const errorContainer = /** @type {HTMLElement} */ (this._errorContainer);
+ errorContainer.hidden = false;
+ errorContainer.textContent = e instanceof Error ? e.message : `${e}`;
}
if (!added) { return false; }
await this._updatePermissions();
diff --git a/ext/js/pages/settings/permissions-toggle-controller.js b/ext/js/pages/settings/permissions-toggle-controller.js
index 0e486c1e..85752a7e 100644
--- a/ext/js/pages/settings/permissions-toggle-controller.js
+++ b/ext/js/pages/settings/permissions-toggle-controller.js
@@ -19,11 +19,17 @@
import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js';
export class PermissionsToggleController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?NodeListOf<HTMLInputElement>} */
this._toggles = null;
}
+ /** */
async prepare() {
this._toggles = document.querySelectorAll('.permissions-toggle');
@@ -34,14 +40,18 @@ export class PermissionsToggleController {
this._settingsController.on('permissionsChanged', this._onPermissionsChanged.bind(this));
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
let accessor = null;
- for (const toggle of this._toggles) {
+ for (const toggle of /** @type {NodeListOf<HTMLInputElement>} */ (this._toggles)) {
const {permissionsSetting} = toggle.dataset;
if (typeof permissionsSetting !== 'string') { continue; }
@@ -61,8 +71,11 @@ export class PermissionsToggleController {
this._updateValidity();
}
+ /**
+ * @param {Event} e
+ */
async _onPermissionsToggleChange(e) {
- const toggle = e.currentTarget;
+ const toggle = /** @type {HTMLInputElement} */ (e.currentTarget);
let value = toggle.checked;
const valuePre = !value;
const {permissionsSetting} = toggle.dataset;
@@ -90,9 +103,13 @@ export class PermissionsToggleController {
}
}
- _onPermissionsChanged({permissions: {permissions}}) {
- const permissionsSet = new Set(permissions);
- for (const toggle of this._toggles) {
+ /**
+ * @param {import('settings-controller').PermissionsChangedEvent} details
+ */
+ _onPermissionsChanged({permissions}) {
+ const permissions2 = permissions.permissions;
+ const permissionsSet = new Set(typeof permissions2 !== 'undefined' ? permissions2 : []);
+ for (const toggle of /** @type {NodeListOf<HTMLInputElement>} */ (this._toggles)) {
const {permissionsSetting} = toggle.dataset;
const hasPermissions = this._hasAll(permissionsSet, this._getRequiredPermissions(toggle));
@@ -105,17 +122,27 @@ export class PermissionsToggleController {
}
}
+ /**
+ * @param {HTMLInputElement} toggle
+ * @param {boolean} valid
+ */
_setToggleValid(toggle, valid) {
- const relative = toggle.closest('.settings-item');
+ const relative = /** @type {?HTMLElement} */ (toggle.closest('.settings-item'));
if (relative === null) { return; }
relative.dataset.invalid = `${!valid}`;
}
+ /** */
async _updateValidity() {
const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
this._onPermissionsChanged({permissions});
}
+ /**
+ * @param {Set<string>} set
+ * @param {string[]} values
+ * @returns {boolean}
+ */
_hasAll(set, values) {
for (const value of values) {
if (!set.has(value)) { return false; }
@@ -123,6 +150,10 @@ export class PermissionsToggleController {
return true;
}
+ /**
+ * @param {HTMLInputElement} toggle
+ * @returns {string[]}
+ */
_getRequiredPermissions(toggle) {
const requiredPermissions = toggle.dataset.requiredPermissions;
return (typeof requiredPermissions === 'string' && requiredPermissions.length > 0 ? requiredPermissions.split(' ') : []);
diff --git a/ext/js/pages/settings/persistent-storage-controller.js b/ext/js/pages/settings/persistent-storage-controller.js
index aa060c14..e85bfc6b 100644
--- a/ext/js/pages/settings/persistent-storage-controller.js
+++ b/ext/js/pages/settings/persistent-storage-controller.js
@@ -21,22 +21,27 @@ import {yomitan} from '../../yomitan.js';
export class PersistentStorageController {
constructor() {
- this._persistentStorageCheckbox = false;
+ /** @type {?HTMLInputElement} */
+ this._persistentStorageCheckbox = null;
}
+ /** */
async prepare() {
- this._persistentStorageCheckbox = document.querySelector('#storage-persistent-checkbox');
+ this._persistentStorageCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#storage-persistent-checkbox'));
this._persistentStorageCheckbox.addEventListener('change', this._onPersistentStorageCheckboxChange.bind(this), false);
if (!this._isPersistentStorageSupported()) { return; }
- const info = document.querySelector('#storage-persistent-info');
+ const info = /** @type {?HTMLElement} */ (document.querySelector('#storage-persistent-info'));
if (info !== null) { info.hidden = false; }
const isStoragePeristent = await this.isStoragePeristent();
this._updateCheckbox(isStoragePeristent);
}
+ /**
+ * @returns {Promise<boolean>}
+ */
async isStoragePeristent() {
try {
return await navigator.storage.persisted();
@@ -48,8 +53,11 @@ export class PersistentStorageController {
// Private
+ /**
+ * @param {Event} e
+ */
_onPersistentStorageCheckboxChange(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
if (node.checked) {
node.checked = false;
this._attemptPersistStorage();
@@ -58,6 +66,7 @@ export class PersistentStorageController {
}
}
+ /** */
async _attemptPersistStorage() {
let isStoragePeristent = false;
try {
@@ -68,18 +77,24 @@ export class PersistentStorageController {
this._updateCheckbox(isStoragePeristent);
- const node = document.querySelector('#storage-persistent-fail-warning');
+ const node = /** @type {?HTMLElement} */ (document.querySelector('#storage-persistent-fail-warning'));
if (node !== null) { node.hidden = isStoragePeristent; }
yomitan.trigger('storageChanged');
}
+ /**
+ * @returns {boolean}
+ */
_isPersistentStorageSupported() {
return isObject(navigator.storage) && typeof navigator.storage.persist === 'function';
}
+ /**
+ * @param {boolean} isStoragePeristent
+ */
_updateCheckbox(isStoragePeristent) {
- this._persistentStorageCheckbox.checked = isStoragePeristent;
- this._persistentStorageCheckbox.readOnly = isStoragePeristent;
+ /** @type {HTMLInputElement} */ (this._persistentStorageCheckbox).checked = isStoragePeristent;
+ /** @type {HTMLInputElement} */ (this._persistentStorageCheckbox).readOnly = isStoragePeristent;
}
}
diff --git a/ext/js/pages/settings/popup-preview-controller.js b/ext/js/pages/settings/popup-preview-controller.js
index a0cb696e..7239ca17 100644
--- a/ext/js/pages/settings/popup-preview-controller.js
+++ b/ext/js/pages/settings/popup-preview-controller.js
@@ -17,22 +17,32 @@
*/
export class PopupPreviewController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
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;
}
+ /** */
async prepare() {
if (new URLSearchParams(location.search).get('popup-preview') === 'false') { return; }
- this._frame = document.querySelector('#popup-preview-frame');
- this._customCss = document.querySelector('#custom-popup-css');
- this._customOuterCss = document.querySelector('#custom-popup-outer-css');
- this._previewFrameContainer = document.querySelector('.preview-frame-container');
+ 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);
@@ -46,25 +56,35 @@ export class PopupPreviewController {
// Private
+ /** */
_onFrameLoad() {
this._onOptionsContextChange();
this._onCustomCssChange();
this._onCustomOuterCssChange();
}
+ /** */
_onCustomCssChange() {
- this._invoke('PopupPreviewFrame.setCustomCss', {css: this._customCss.value});
+ const css = /** @type {HTMLTextAreaElement} */ (this._customCss).value;
+ this._invoke('PopupPreviewFrame.setCustomCss', {css});
}
+ /** */
_onCustomOuterCssChange() {
- this._invoke('PopupPreviewFrame.setCustomOuterCss', {css: this._customOuterCss.value});
+ const css = /** @type {HTMLTextAreaElement} */ (this._customOuterCss).value;
+ this._invoke('PopupPreviewFrame.setCustomOuterCss', {css});
}
+ /** */
_onOptionsContextChange() {
const optionsContext = this._settingsController.getOptionsContext();
this._invoke('PopupPreviewFrame.updateOptionsContext', {optionsContext});
}
+ /**
+ * @param {string} action
+ * @param {import('core').SerializableObject} params
+ */
_invoke(action, params) {
if (this._frame === null || this._frame.contentWindow === null) { return; }
this._frame.contentWindow.postMessage({action, params}, this._targetOrigin);
diff --git a/ext/js/pages/settings/popup-preview-frame-main.js b/ext/js/pages/settings/popup-preview-frame-main.js
index 59e409c5..bce485fe 100644
--- a/ext/js/pages/settings/popup-preview-frame-main.js
+++ b/ext/js/pages/settings/popup-preview-frame-main.js
@@ -27,6 +27,12 @@ import {PopupPreviewFrame} from './popup-preview-frame.js';
await yomitan.prepare();
const {tabId, frameId} = await yomitan.api.frameInformationGet();
+ if (typeof tabId === 'undefined') {
+ throw new Error('Failed to get tabId');
+ }
+ if (typeof frameId === 'undefined') {
+ throw new Error('Failed to get frameId');
+ }
const hotkeyHandler = new HotkeyHandler();
hotkeyHandler.prepare();
diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js
index 7a1a0b3a..60d264fa 100644
--- a/ext/js/pages/settings/popup-preview-frame.js
+++ b/ext/js/pages/settings/popup-preview-frame.js
@@ -22,32 +22,53 @@ import {TextSourceRange} from '../../dom/text-source-range.js';
import {yomitan} from '../../yomitan.js';
export class PopupPreviewFrame {
+ /**
+ * @param {number} tabId
+ * @param {number} frameId
+ * @param {import('../../app/popup-factory.js').PopupFactory} popupFactory
+ * @param {import('../../input/hotkey-handler.js').HotkeyHandler} hotkeyHandler
+ */
constructor(tabId, frameId, popupFactory, hotkeyHandler) {
+ /** @type {number} */
this._tabId = tabId;
+ /** @type {number} */
this._frameId = frameId;
+ /** @type {import('../../app/popup-factory.js').PopupFactory} */
this._popupFactory = popupFactory;
+ /** @type {import('../../input/hotkey-handler.js').HotkeyHandler} */
this._hotkeyHandler = hotkeyHandler;
+ /** @type {?Frontend} */
this._frontend = null;
+ /** @type {?(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */
this._apiOptionsGetOld = null;
+ /** @type {boolean} */
this._popupShown = false;
+ /** @type {?import('core').Timeout} */
this._themeChangeTimeout = null;
+ /** @type {?import('text-source').TextSource} */
this._textSource = null;
+ /** @type {?import('settings').OptionsContext} */
this._optionsContext = null;
+ /** @type {?HTMLElement} */
this._exampleText = null;
+ /** @type {?HTMLInputElement} */
this._exampleTextInput = null;
+ /** @type {string} */
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
- this._windowMessageHandlers = new Map([
+ /** @type {Map<string, (params: import('core').SerializableObjectAny) => void>} */
+ this._windowMessageHandlers = new Map(/** @type {[key: string, handler: (params: import('core').SerializableObjectAny) => void][]} */ ([
['PopupPreviewFrame.setText', this._onSetText.bind(this)],
['PopupPreviewFrame.setCustomCss', this._setCustomCss.bind(this)],
['PopupPreviewFrame.setCustomOuterCss', this._setCustomOuterCss.bind(this)],
['PopupPreviewFrame.updateOptionsContext', this._updateOptionsContext.bind(this)]
- ]);
+ ]));
}
+ /** */
async prepare() {
- this._exampleText = document.querySelector('#example-text');
- this._exampleTextInput = document.querySelector('#example-text-input');
+ 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);
@@ -56,12 +77,14 @@ export class PopupPreviewFrame {
window.addEventListener('message', this._onMessage.bind(this), false);
// Setup events
- document.querySelector('#theme-dark-checkbox').addEventListener('change', this._onThemeDarkCheckboxChanged.bind(this), false);
+ const darkThemeCheckbox = /** @type {HTMLInputElement} */ (document.querySelector('#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);
this._exampleTextInput.addEventListener('input', this._onExampleTextInputInput.bind(this), false);
// Overwrite API functions
+ /** @type {?(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */
this._apiOptionsGetOld = yomitan.api.optionsGet.bind(yomitan.api);
yomitan.api.optionsGet = this._apiOptionsGet.bind(this);
@@ -84,7 +107,10 @@ export class PopupPreviewFrame {
await this._frontend.prepare();
this._frontend.setDisabledOverride(true);
this._frontend.canClearSelection = false;
- this._frontend.popup.on('customOuterCssChanged', this._onCustomOuterCssChanged.bind(this));
+ const {popup} = this._frontend;
+ if (popup !== null) {
+ popup.on('customOuterCssChanged', this._onCustomOuterCssChanged.bind(this));
+ }
// Update search
this._updateSearch();
@@ -92,8 +118,12 @@ export class PopupPreviewFrame {
// Private
- async _apiOptionsGet(...args) {
- const options = await this._apiOptionsGetOld(...args);
+ /**
+ * @param {import('settings').OptionsContext} optionsContext
+ * @returns {Promise<import('settings').ProfileOptions>}
+ */
+ async _apiOptionsGet(optionsContext) {
+ const options = await /** @type {(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */ (this._apiOptionsGetOld)(optionsContext);
options.general.enable = true;
options.general.debugInfo = false;
options.general.popupWidth = 400;
@@ -108,16 +138,24 @@ export class PopupPreviewFrame {
return options;
}
+ /**
+ * @param {import('popup').CustomOuterCssChangedEvent} details
+ */
_onCustomOuterCssChanged({node, inShadow}) {
if (node === null || inShadow) { return; }
const node2 = document.querySelector('#popup-outer-css');
if (node2 === null) { return; }
+ const {parentNode} = node2;
+ if (parentNode === null) { return; }
// This simulates the stylesheet priorities when injecting using the web extension API.
- node2.parentNode.insertBefore(node, node2);
+ parentNode.insertBefore(node, node2);
}
+ /**
+ * @param {MessageEvent<{action: string, params: import('core').SerializableObject}>} e
+ */
_onMessage(e) {
if (e.origin !== this._targetOrigin) { return; }
@@ -128,19 +166,24 @@ export class PopupPreviewFrame {
handler(params);
}
+ /**
+ * @param {Event} e
+ */
_onThemeDarkCheckboxChanged(e) {
- document.documentElement.classList.toggle('dark', e.target.checked);
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ document.documentElement.classList.toggle('dark', element.checked);
if (this._themeChangeTimeout !== null) {
clearTimeout(this._themeChangeTimeout);
}
this._themeChangeTimeout = setTimeout(() => {
this._themeChangeTimeout = null;
- const popup = this._frontend.popup;
+ const popup = /** @type {Frontend} */ (this._frontend).popup;
if (popup === null) { return; }
popup.updateTheme();
}, 300);
}
+ /** */
_onExampleTextClick() {
if (this._exampleTextInput === null) { return; }
const visible = this._exampleTextInput.hidden;
@@ -150,19 +193,31 @@ export class PopupPreviewFrame {
this._exampleTextInput.select();
}
+ /** */
_onExampleTextInputBlur() {
if (this._exampleTextInput === null) { return; }
this._exampleTextInput.hidden = true;
}
+ /**
+ * @param {Event} e
+ */
_onExampleTextInputInput(e) {
- this._setText(e.currentTarget.value);
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ this._setText(element.value, false);
}
+ /**
+ * @param {{text: string}} details
+ */
_onSetText({text}) {
this._setText(text, true);
}
+ /**
+ * @param {string} text
+ * @param {boolean} setInput
+ */
_setText(text, setInput) {
if (setInput && this._exampleTextInput !== null) {
this._exampleTextInput.value = text;
@@ -175,6 +230,9 @@ export class PopupPreviewFrame {
this._updateSearch();
}
+ /**
+ * @param {boolean} visible
+ */
_setInfoVisible(visible) {
const node = document.querySelector('.placeholder-info');
if (node === null) { return; }
@@ -182,6 +240,9 @@ export class PopupPreviewFrame {
node.classList.toggle('placeholder-info-visible', visible);
}
+ /**
+ * @param {{css: string}} details
+ */
_setCustomCss({css}) {
if (this._frontend === null) { return; }
const popup = this._frontend.popup;
@@ -189,6 +250,9 @@ export class PopupPreviewFrame {
popup.setCustomCss(css);
}
+ /**
+ * @param {{css: string}} details
+ */
_setCustomOuterCss({css}) {
if (this._frontend === null) { return; }
const popup = this._frontend.popup;
@@ -196,7 +260,11 @@ export class PopupPreviewFrame {
popup.setCustomOuterCss(css, false);
}
- async _updateOptionsContext({optionsContext}) {
+ /**
+ * @param {{optionsContext: import('settings').OptionsContext}} details
+ */
+ async _updateOptionsContext(details) {
+ const {optionsContext} = details;
this._optionsContext = optionsContext;
if (this._frontend === null) { return; }
this._frontend.setOptionsContextOverride(optionsContext);
@@ -204,6 +272,7 @@ export class PopupPreviewFrame {
await this._updateSearch();
}
+ /** */
async _updateSearch() {
if (this._exampleText === null) { return; }
@@ -213,16 +282,17 @@ export class PopupPreviewFrame {
const range = document.createRange();
range.selectNodeContents(textNode);
const source = TextSourceRange.create(range);
+ const frontend = /** @type {Frontend} */ (this._frontend);
try {
- await this._frontend.setTextSource(source);
+ await frontend.setTextSource(source);
} finally {
source.cleanup();
}
this._textSource = source;
- await this._frontend.showContentCompleted();
+ await frontend.showContentCompleted();
- const popup = this._frontend.popup;
+ const popup = frontend.popup;
if (popup !== null && popup.isVisibleSync()) {
this._popupShown = true;
}
diff --git a/ext/js/pages/settings/popup-window-controller.js b/ext/js/pages/settings/popup-window-controller.js
index 9b6708d5..e1a5456b 100644
--- a/ext/js/pages/settings/popup-window-controller.js
+++ b/ext/js/pages/settings/popup-window-controller.js
@@ -19,18 +19,23 @@
import {yomitan} from '../../yomitan.js';
export class PopupWindowController {
+ /** */
prepare() {
- const testLink = document.querySelector('#test-window-open-link');
+ const testLink = /** @type {HTMLElement} */ (document.querySelector('#test-window-open-link'));
testLink.addEventListener('click', this._onTestWindowOpenLinkClick.bind(this), false);
}
// Private
+ /**
+ * @param {MouseEvent} e
+ */
_onTestWindowOpenLinkClick(e) {
e.preventDefault();
this._testWindowOpen();
}
+ /** */
async _testWindowOpen() {
await yomitan.api.getOrCreateSearchPopup({focus: true});
}
diff --git a/ext/js/pages/settings/profile-conditions-ui.js b/ext/js/pages/settings/profile-conditions-ui.js
index bd790b1b..96aef83f 100644
--- a/ext/js/pages/settings/profile-conditions-ui.js
+++ b/ext/js/pages/settings/profile-conditions-ui.js
@@ -17,23 +17,39 @@
*/
import {EventDispatcher, EventListenerCollection} from '../../core.js';
+import {DocumentUtil} from '../../dom/document-util.js';
import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';
+/**
+ * @augments EventDispatcher<import('profile-conditions-ui').EventType>
+ */
export class ProfileConditionsUI extends EventDispatcher {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
super();
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?import('environment').OperatingSystem} */
this._os = null;
+ /** @type {?HTMLElement} */
this._conditionGroupsContainer = null;
+ /** @type {?HTMLElement} */
this._addConditionGroupButton = null;
+ /** @type {ProfileConditionGroupUI[]} */
this._children = [];
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {import('profile-conditions-ui').DescriptorType} */
this._defaultType = 'popupLevel';
+ /** @type {number} */
this._profileIndex = 0;
const validateInteger = this._validateInteger.bind(this);
const normalizeInteger = this._normalizeInteger.bind(this);
const validateFlags = this._validateFlags.bind(this);
const normalizeFlags = this._normalizeFlags.bind(this);
+ /** @type {Map<import('profile-conditions-ui').DescriptorType, import('profile-conditions-ui').Descriptor>} */
this._descriptors = new Map([
[
'popupLevel',
@@ -88,19 +104,23 @@ export class ProfileConditionsUI extends EventDispatcher {
}
]
]);
+ /** @type {Set<string>} */
this._validFlags = new Set([
'clipboard'
]);
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._settingsController;
}
+ /** @type {number} */
get profileIndex() {
return this._profileIndex;
}
+ /** @type {?import('environment').OperatingSystem} */
get os() {
return this._os;
}
@@ -109,6 +129,9 @@ export class ProfileConditionsUI extends EventDispatcher {
this._os = value;
}
+ /**
+ * @param {number} profileIndex
+ */
async prepare(profileIndex) {
const options = await this._settingsController.getOptionsFull();
const {profiles} = options;
@@ -116,8 +139,8 @@ export class ProfileConditionsUI extends EventDispatcher {
const {conditionGroups} = profiles[profileIndex];
this._profileIndex = profileIndex;
- this._conditionGroupsContainer = document.querySelector('#profile-condition-groups');
- this._addConditionGroupButton = document.querySelector('#profile-add-condition-group');
+ 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);
@@ -126,6 +149,7 @@ export class ProfileConditionsUI extends EventDispatcher {
this._eventListeners.addEventListener(this._addConditionGroupButton, 'click', this._onAddConditionGroupButtonClick.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
@@ -138,10 +162,17 @@ export class ProfileConditionsUI extends EventDispatcher {
this._addConditionGroupButton = null;
}
- instantiateTemplate(names) {
- return this._settingsController.instantiateTemplate(names);
+ /**
+ * @param {string} name
+ * @returns {HTMLElement}
+ */
+ instantiateTemplate(name) {
+ return /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate(name));
}
+ /**
+ * @returns {import('profile-conditions-ui').DescriptorInfo[]}
+ */
getDescriptorTypes() {
const results = [];
for (const [name, {displayName}] of this._descriptors.entries()) {
@@ -150,6 +181,10 @@ export class ProfileConditionsUI extends EventDispatcher {
return results;
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @returns {import('profile-conditions-ui').OperatorInfo[]}
+ */
getDescriptorOperators(type) {
const info = this._descriptors.get(type);
const results = [];
@@ -161,15 +196,27 @@ export class ProfileConditionsUI extends EventDispatcher {
return results;
}
+ /**
+ * @returns {import('profile-conditions-ui').DescriptorType}
+ */
getDefaultType() {
return this._defaultType;
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @returns {string}
+ */
getDefaultOperator(type) {
const info = this._descriptors.get(type);
return (typeof info !== 'undefined' ? info.defaultOperator : '');
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} operator
+ * @returns {import('profile-conditions-ui').Operator}
+ */
getOperatorDetails(type, operator) {
const info = this._getOperatorDetails(type, operator);
@@ -192,6 +239,9 @@ export class ProfileConditionsUI extends EventDispatcher {
};
}
+ /**
+ * @returns {import('settings').ProfileCondition}
+ */
getDefaultCondition() {
const type = this.getDefaultType();
const operator = this.getDefaultOperator(type);
@@ -199,6 +249,10 @@ export class ProfileConditionsUI extends EventDispatcher {
return {type, operator, value};
}
+ /**
+ * @param {ProfileConditionGroupUI} child
+ * @returns {boolean}
+ */
removeConditionGroup(child) {
const index = child.index;
if (index < 0 || index >= this._children.length) { return false; }
@@ -226,22 +280,53 @@ export class ProfileConditionsUI extends EventDispatcher {
return true;
}
+ /**
+ * @param {string} value
+ * @returns {string[]}
+ */
splitValue(value) {
return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
}
+ /**
+ * @param {string} property
+ * @returns {string}
+ */
getPath(property) {
property = (typeof property === 'string' ? `.${property}` : '');
return `profiles[${this.profileIndex}]${property}`;
}
+ /**
+ * @param {HTMLInputElement} inputNode
+ * @param {?HTMLButtonElement} mouseButton
+ * @returns {KeyboardMouseInputField}
+ */
createKeyboardMouseInputField(inputNode, mouseButton) {
return new KeyboardMouseInputField(inputNode, mouseButton, this._os);
}
+ /**
+ * @param {string} value
+ * @returns {?import('settings').ProfileConditionType}
+ */
+ static normalizeProfileConditionType(value) {
+ switch (value) {
+ case 'popupLevel':
+ case 'url':
+ case 'modifierKeys':
+ case 'flags':
+ return value;
+ default:
+ return null;
+ }
+ }
+
// Private
+ /** */
_onAddConditionGroupButtonClick() {
+ /** @type {import('settings').ProfileConditionGroup} */
const conditionGroup = {
conditions: [this.getDefaultCondition()]
};
@@ -260,28 +345,50 @@ export class ProfileConditionsUI extends EventDispatcher {
this._triggerConditionGroupCountChanged(this._children.length);
}
+ /**
+ * @param {import('settings').ProfileConditionGroup} conditionGroup
+ * @param {number} index
+ * @returns {ProfileConditionGroupUI}
+ */
_addConditionGroup(conditionGroup, index) {
const child = new ProfileConditionGroupUI(this, index);
child.prepare(conditionGroup);
this._children.push(child);
- this._conditionGroupsContainer.appendChild(child.node);
+ /** @type {HTMLElement} */ (this._conditionGroupsContainer).appendChild(child.node);
return child;
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} operator
+ * @returns {import('profile-conditions-ui').OperatorInternal|undefined}
+ */
_getOperatorDetails(type, operator) {
const info = this._descriptors.get(type);
return (typeof info !== 'undefined' ? info.operators.get(operator) : void 0);
}
+ /**
+ * @param {string} value
+ * @returns {boolean}
+ */
_validateInteger(value) {
const number = Number.parseFloat(value);
return Number.isFinite(number) && Math.floor(number) === number;
}
+ /**
+ * @param {string} value
+ * @returns {boolean}
+ */
_validateDomains(value) {
return this.splitValue(value).length > 0;
}
+ /**
+ * @param {string} value
+ * @returns {boolean}
+ */
_validateRegExp(value) {
try {
new RegExp(value, 'i');
@@ -291,15 +398,27 @@ export class ProfileConditionsUI extends EventDispatcher {
}
}
+ /**
+ * @param {string} value
+ * @returns {string}
+ */
_normalizeInteger(value) {
const number = Number.parseFloat(value);
return `${number}`;
}
+ /**
+ * @param {string} value
+ * @returns {string}
+ */
_normalizeDomains(value) {
return this.splitValue(value).join(', ');
}
+ /**
+ * @param {string} value
+ * @returns {boolean}
+ */
_validateFlags(value) {
const flags = this.splitValue(value);
for (const flag of flags) {
@@ -310,34 +429,57 @@ export class ProfileConditionsUI extends EventDispatcher {
return flags.length > 0;
}
+ /**
+ * @param {string} value
+ * @returns {string}
+ */
_normalizeFlags(value) {
return [...new Set(this.splitValue(value))].join(', ');
}
+ /**
+ * @param {number} count
+ */
_triggerConditionGroupCountChanged(count) {
- this.trigger('conditionGroupCountChanged', {count, profileIndex: this._profileIndex});
+ /** @type {import('profile-conditions-ui').ConditionGroupCountChangedEvent} */
+ const event = {count, profileIndex: this._profileIndex};
+ this.trigger('conditionGroupCountChanged', event);
}
}
class ProfileConditionGroupUI {
+ /**
+ * @param {ProfileConditionsUI} parent
+ * @param {number} index
+ */
constructor(parent, index) {
+ /** @type {ProfileConditionsUI} */
this._parent = parent;
+ /** @type {number} */
this._index = index;
- this._node = null;
- this._conditionContainer = null;
- this._addConditionButton = null;
+ /** @type {HTMLElement} */
+ this._node = /** @type {HTMLElement} */ (this._parent.instantiateTemplate('profile-condition-group'));
+ /** @type {HTMLElement} */
+ this._conditionContainer = /** @type {HTMLElement} */ (this._node.querySelector('.profile-condition-list'));
+ /** @type {HTMLElement} */
+ this._addConditionButton = /** @type {HTMLElement} */ (this._node.querySelector('.profile-condition-add-button'));
+ /** @type {ProfileConditionUI[]} */
this._children = [];
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._parent.settingsController;
}
+ /** @type {ProfileConditionsUI} */
get parent() {
return this._parent;
}
+ /** @type {number} */
get index() {
return this._index;
}
@@ -346,19 +488,20 @@ class ProfileConditionGroupUI {
this._index = value;
}
+ /** @type {HTMLElement} */
get node() {
return this._node;
}
+ /** @type {number} */
get childCount() {
return this._children.length;
}
+ /**
+ * @param {import('settings').ProfileConditionGroup} conditionGroup
+ */
prepare(conditionGroup) {
- this._node = this._parent.instantiateTemplate('profile-condition-group');
- this._conditionContainer = this._node.querySelector('.profile-condition-list');
- this._addConditionButton = this._node.querySelector('.profile-condition-add-button');
-
const conditions = conditionGroup.conditions;
for (let i = 0, ii = conditions.length; i < ii; ++i) {
this._addCondition(conditions[i], i);
@@ -367,6 +510,7 @@ class ProfileConditionGroupUI {
this._eventListeners.addEventListener(this._addConditionButton, 'click', this._onAddConditionButtonClick.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
@@ -378,15 +522,15 @@ class ProfileConditionGroupUI {
if (this._node === null) { return; }
const node = this._node;
- this._node = null;
- this._conditionContainer = null;
- this._addConditionButton = null;
-
if (node.parentNode !== null) {
node.parentNode.removeChild(node);
}
}
+ /**
+ * @param {ProfileConditionUI} child
+ * @returns {boolean}
+ */
removeCondition(child) {
const index = child.index;
if (index < 0 || index >= this._children.length) { return false; }
@@ -416,17 +560,23 @@ class ProfileConditionGroupUI {
return true;
}
+ /**
+ * @param {string} property
+ * @returns {string}
+ */
getPath(property) {
property = (typeof property === 'string' ? `.${property}` : '');
return this._parent.getPath(`conditionGroups[${this._index}]${property}`);
}
+ /** */
removeSelf() {
this._parent.removeConditionGroup(this);
}
// Private
+ /** */
_onAddConditionButtonClick() {
const condition = this._parent.getDefaultCondition();
const index = this._children.length;
@@ -442,41 +592,73 @@ class ProfileConditionGroupUI {
}]);
}
+ /**
+ * @param {import('settings').ProfileCondition} condition
+ * @param {number} index
+ * @returns {ProfileConditionUI}
+ */
_addCondition(condition, index) {
const child = new ProfileConditionUI(this, index);
child.prepare(condition);
this._children.push(child);
- this._conditionContainer.appendChild(child.node);
+ if (this._conditionContainer !== null) {
+ this._conditionContainer.appendChild(child.node);
+ }
return child;
}
}
class ProfileConditionUI {
+ /**
+ * @param {ProfileConditionGroupUI} parent
+ * @param {number} index
+ */
constructor(parent, index) {
+ /** @type {ProfileConditionGroupUI} */
this._parent = parent;
+ /** @type {number} */
this._index = index;
- this._node = null;
- this._typeInput = null;
- this._operatorInput = null;
- this._valueInputContainer = null;
- this._removeButton = null;
- this._mouseButton = null;
- this._mouseButtonContainer = null;
- this._menuButton = null;
+ /** @type {HTMLElement} */
+ this._node = this._parent.parent.instantiateTemplate('profile-condition');
+ /** @type {HTMLSelectElement} */
+ this._typeInput = /** @type {HTMLSelectElement} */ (this._node.querySelector('.profile-condition-type'));
+ /** @type {HTMLSelectElement} */
+ this._operatorInput = /** @type {HTMLSelectElement} */ (this._node.querySelector('.profile-condition-operator'));
+ /** @type {HTMLButtonElement} */
+ this._removeButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.profile-condition-remove'));
+ /** @type {HTMLButtonElement} */
+ this._mouseButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.mouse-button'));
+ /** @type {HTMLElement} */
+ this._mouseButtonContainer = /** @type {HTMLElement} */ (this._node.querySelector('.mouse-button-container'));
+ /** @type {HTMLButtonElement} */
+ this._menuButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.profile-condition-menu-button'));
+ /** @type {HTMLElement} */
+ this._typeOptionContainer = /** @type {HTMLElement} */ (this._typeInput.querySelector('optgroup'));
+ /** @type {HTMLElement} */
+ this._operatorOptionContainer = /** @type {HTMLElement} */ (this._operatorInput.querySelector('optgroup'));
+ /** @type {HTMLInputElement} */
+ this._valueInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.profile-condition-input'));
+ /** @type {string} */
this._value = '';
+ /** @type {?KeyboardMouseInputField} */
this._kbmInputField = null;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {EventListenerCollection} */
this._inputEventListeners = new EventListenerCollection();
}
+ /** @type {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._parent.parent.settingsController;
}
+ /** @type {ProfileConditionGroupUI} */
get parent() {
return this._parent;
}
+ /** @type {number} */
get index() {
return this._index;
}
@@ -485,24 +667,17 @@ class ProfileConditionUI {
this._index = value;
}
+ /** @type {HTMLElement} */
get node() {
return this._node;
}
+ /**
+ * @param {import('settings').ProfileCondition} condition
+ */
prepare(condition) {
const {type, operator, value} = condition;
- this._node = this._parent.parent.instantiateTemplate('profile-condition');
- this._typeInput = this._node.querySelector('.profile-condition-type');
- this._typeOptionContainer = this._typeInput.querySelector('optgroup');
- this._operatorInput = this._node.querySelector('.profile-condition-operator');
- this._operatorOptionContainer = this._operatorInput.querySelector('optgroup');
- this._valueInput = this._node.querySelector('.profile-condition-input');
- this._removeButton = this._node.querySelector('.profile-condition-remove');
- this._mouseButton = this._node.querySelector('.mouse-button');
- this._mouseButtonContainer = this._node.querySelector('.mouse-button-container');
- this._menuButton = this._node.querySelector('.profile-condition-menu-button');
-
const operatorDetails = this._getOperatorDetails(type, operator);
this._updateTypes(type);
this._updateOperators(type, operator);
@@ -517,6 +692,7 @@ class ProfileConditionUI {
}
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
this._value = '';
@@ -524,17 +700,15 @@ class ProfileConditionUI {
if (this._node === null) { return; }
const node = this._node;
- this._node = null;
- this._typeInput = null;
- this._operatorInput = null;
- this._valueInputContainer = null;
- this._removeButton = null;
-
if (node.parentNode !== null) {
node.parentNode.removeChild(node);
}
}
+ /**
+ * @param {string} property
+ * @returns {string}
+ */
getPath(property) {
property = (typeof property === 'string' ? `.${property}` : '');
return this._parent.getPath(`conditions[${this._index}]${property}`);
@@ -542,19 +716,33 @@ class ProfileConditionUI {
// Private
+ /**
+ * @param {Event} e
+ */
_onTypeChange(e) {
- const type = e.currentTarget.value;
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const type = ProfileConditionsUI.normalizeProfileConditionType(element.value);
+ if (type === null) { return; }
this._setType(type);
}
+ /**
+ * @param {Event} e
+ */
_onOperatorChange(e) {
- const type = this._typeInput.value;
- const operator = e.currentTarget.value;
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const type = ProfileConditionsUI.normalizeProfileConditionType(this._typeInput.value);
+ if (type === null) { return; }
+ const operator = element.value;
this._setOperator(type, operator);
}
+ /**
+ * @param {import('profile-conditions-ui').InputData} details
+ * @param {Event} e
+ */
_onValueInputChange({validate, normalize}, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
const value = node.value;
const okay = this._validateValue(value, validate);
this._value = value;
@@ -565,8 +753,12 @@ class ProfileConditionUI {
}
}
- _onModifierInputChange({validate, normalize}, {modifiers}) {
- modifiers = this._joinModifiers(modifiers);
+ /**
+ * @param {import('profile-conditions-ui').InputData} details
+ * @param {import('keyboard-mouse-input-field').ChangeEvent} event
+ */
+ _onModifierInputChange({validate, normalize}, event) {
+ const modifiers = this._joinModifiers(event.modifiers);
const okay = this._validateValue(modifiers, validate);
this._value = modifiers;
if (okay) {
@@ -575,18 +767,25 @@ class ProfileConditionUI {
}
}
+ /** */
_onRemoveButtonClick() {
this._removeSelf();
}
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const bodyNode = e.detail.menu.bodyNode;
- const deleteGroup = bodyNode.querySelector('.popup-menu-item[data-menu-action="deleteGroup"]');
+ const deleteGroup = /** @type {HTMLElement} */ (bodyNode.querySelector('.popup-menu-item[data-menu-action="deleteGroup"]'));
if (deleteGroup !== null) {
deleteGroup.hidden = (this._parent.childCount <= 1);
}
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'delete':
@@ -601,28 +800,53 @@ class ProfileConditionUI {
}
}
+ /**
+ * @returns {import('profile-conditions-ui').DescriptorInfo[]}
+ */
_getDescriptorTypes() {
return this._parent.parent.getDescriptorTypes();
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @returns {import('profile-conditions-ui').OperatorInfo[]}
+ */
_getDescriptorOperators(type) {
return this._parent.parent.getDescriptorOperators(type);
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} operator
+ * @returns {import('profile-conditions-ui').Operator}
+ */
_getOperatorDetails(type, operator) {
return this._parent.parent.getOperatorDetails(type, operator);
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ */
_updateTypes(type) {
const types = this._getDescriptorTypes();
this._updateSelect(this._typeInput, this._typeOptionContainer, types, type);
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} operator
+ */
_updateOperators(type, operator) {
const operators = this._getDescriptorOperators(type);
this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator);
}
+ /**
+ * @param {HTMLSelectElement} select
+ * @param {HTMLElement} optionContainer
+ * @param {import('profile-conditions-ui').DescriptorInfo[]|import('profile-conditions-ui').OperatorInfo[]} values
+ * @param {string} value
+ */
_updateSelect(select, optionContainer, values, value) {
optionContainer.textContent = '';
for (const {name, displayName} of values) {
@@ -634,6 +858,11 @@ class ProfileConditionUI {
select.value = value;
}
+ /**
+ * @param {string} value
+ * @param {import('profile-conditions-ui').Operator} operator
+ * @returns {boolean}
+ */
_updateValueInput(value, {type, validate, normalize}) {
this._inputEventListeners.removeAllEventListeners();
if (this._kbmInputField !== null) {
@@ -642,10 +871,15 @@ class ProfileConditionUI {
}
let inputType = 'text';
+ /** @type {?string} */
let inputValue = value;
let inputStep = null;
let showMouseButton = false;
- const events = [];
+ /** @type {import('event-listener-collection').AddEventListenerArgs[]} */
+ const events1 = [];
+ /** @type {import('event-listener-collection').OnArgs[]} */
+ const events2 = [];
+ /** @type {import('profile-conditions-ui').InputData} */
const inputData = {validate, normalize};
const node = this._valueInput;
@@ -653,7 +887,7 @@ class ProfileConditionUI {
case 'integer':
inputType = 'number';
inputStep = '1';
- events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]);
+ events1.push([node, 'change', this._onValueInputChange.bind(this, inputData), false]);
break;
case 'modifierKeys':
case 'modifierInputs':
@@ -661,10 +895,10 @@ class ProfileConditionUI {
showMouseButton = (type === 'modifierInputs');
this._kbmInputField = this._parent.parent.createKeyboardMouseInputField(node, this._mouseButton);
this._kbmInputField.prepare(null, this._splitModifiers(value), showMouseButton, false);
- events.push(['on', this._kbmInputField, 'change', this._onModifierInputChange.bind(this, inputData), false]);
+ events2.push([this._kbmInputField, 'change', this._onModifierInputChange.bind(this, inputData)]);
break;
default: // 'string'
- events.push(['addEventListener', node, 'change', this._onValueInputChange.bind(this, inputData), false]);
+ events1.push([node, 'change', this._onValueInputChange.bind(this, inputData), false]);
break;
}
@@ -680,35 +914,67 @@ class ProfileConditionUI {
node.removeAttribute('step');
}
this._mouseButtonContainer.hidden = !showMouseButton;
- for (const args of events) {
- this._inputEventListeners.addGeneric(...args);
+ for (const args of events1) {
+ this._inputEventListeners.addEventListener(...args);
+ }
+ for (const args of events2) {
+ this._inputEventListeners.on(...args);
}
- this._validateValue(value, validate);
+ return this._validateValue(value, validate);
}
+ /**
+ * @param {string} value
+ * @param {?import('profile-conditions-ui').ValidateFunction} validate
+ * @returns {boolean}
+ */
_validateValue(value, validate) {
const okay = (validate === null || validate(value));
this._valueInput.dataset.invalid = `${!okay}`;
return okay;
}
+ /**
+ * @param {string} value
+ * @param {?import('profile-conditions-ui').NormalizeFunction} normalize
+ * @returns {value}
+ */
_normalizeValue(value, normalize) {
return (normalize !== null ? normalize(value) : value);
}
+ /** */
_removeSelf() {
this._parent.removeCondition(this);
}
+ /**
+ * @param {string} modifiersString
+ * @returns {import('input').Modifier[]}
+ */
_splitModifiers(modifiersString) {
- return modifiersString.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
+ /** @type {import('input').Modifier[]} */
+ const results = [];
+ for (const item of modifiersString.split(/[,;\s]+/)) {
+ const modifier = DocumentUtil.normalizeModifier(item.trim().toLowerCase());
+ if (modifier !== null) { results.push(modifier); }
+ }
+ return results;
}
+ /**
+ * @param {import('input').Modifier[]} modifiersArray
+ * @returns {string}
+ */
_joinModifiers(modifiersArray) {
return modifiersArray.join(', ');
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} [operator]
+ */
async _setType(type, operator) {
const operators = this._getDescriptorOperators(type);
if (typeof operator === 'undefined') {
@@ -725,8 +991,13 @@ class ProfileConditionUI {
]);
}
+ /**
+ * @param {import('profile-conditions-ui').DescriptorType} type
+ * @param {string} operator
+ */
async _setOperator(type, operator) {
const operatorDetails = this._getOperatorDetails(type, operator);
+ /** @type {import('settings-modifications').Modification[]} */
const settingsModifications = [{action: 'set', path: this.getPath('operator'), value: operator}];
if (operatorDetails.resetDefaultOnChange) {
const {defaultValue} = operatorDetails;
@@ -738,8 +1009,10 @@ class ProfileConditionUI {
await this.settingsController.modifyGlobalSettings(settingsModifications);
}
+ /** */
async _resetValue() {
- const type = this._typeInput.value;
+ const type = ProfileConditionsUI.normalizeProfileConditionType(this._typeInput.value);
+ if (type === null) { return; }
const operator = this._operatorInput.value;
await this._setType(type, operator);
}
diff --git a/ext/js/pages/settings/profile-controller.js b/ext/js/pages/settings/profile-controller.js
index a5bf41b3..c82223b8 100644
--- a/ext/js/pages/settings/profile-controller.js
+++ b/ext/js/pages/settings/profile-controller.js
@@ -21,50 +21,77 @@ import {yomitan} from '../../yomitan.js';
import {ProfileConditionsUI} from './profile-conditions-ui.js';
export class ProfileController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ */
constructor(settingsController, modalController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {ProfileConditionsUI} */
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 {?import('./modal.js').Modal} */
this._profileRemoveModal = null;
+ /** @type {?import('./modal.js').Modal} */
this._profileCopyModal = null;
+ /** @type {?import('./modal.js').Modal} */
this._profileConditionsModal = null;
+ /** @type {boolean} */
this._profileEntriesSupported = false;
+ /** @type {ProfileEntry[]} */
this._profileEntryList = [];
+ /** @type {import('settings').Profile[]} */
this._profiles = [];
+ /** @type {number} */
this._profileCurrent = 0;
}
+ /** @type {number} */
get profileCount() {
return this._profiles.length;
}
+ /** @type {number} */
get profileCurrentIndex() {
return this._profileCurrent;
}
+ /** */
async prepare() {
const {platform: {os}} = await yomitan.api.getEnvironmentInfo();
this._profileConditionsUI.os = os;
- this._profileActiveSelect = document.querySelector('#profile-active-select');
- this._profileTargetSelect = document.querySelector('#profile-target-select');
- this._profileCopySourceSelect = document.querySelector('#profile-copy-source-select');
- this._removeProfileNameElement = document.querySelector('#profile-remove-name');
- this._profileAddButton = document.querySelector('#profile-add-button');
- this._profileRemoveConfirmButton = document.querySelector('#profile-remove-confirm-button');
- this._profileCopyConfirmButton = document.querySelector('#profile-copy-confirm-button');
- this._profileEntryListContainer = document.querySelector('#profile-entry-list');
- this._profileConditionsProfileName = document.querySelector('#profile-conditions-profile-name');
+ 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');
@@ -82,6 +109,10 @@ export class ProfileController {
this._onOptionsChanged();
}
+ /**
+ * @param {number} profileIndex
+ * @param {number} offset
+ */
async moveProfile(profileIndex, offset) {
if (this._getProfile(profileIndex) === null) { return; }
@@ -91,6 +122,10 @@ export class ProfileController {
await this.swapProfiles(profileIndex, profileIndexNew);
}
+ /**
+ * @param {number} profileIndex
+ * @param {string} value
+ */
async setProfileName(profileIndex, value) {
const profile = this._getProfile(profileIndex);
if (profile === null) { return; }
@@ -104,11 +139,14 @@ export class ProfileController {
await this._settingsController.setGlobalSetting(`profiles[${profileIndex}].name`, value);
}
+ /**
+ * @param {number} profileIndex
+ */
async setDefaultProfile(profileIndex) {
const profile = this._getProfile(profileIndex);
if (profile === null) { return; }
- this._profileActiveSelect.value = `${profileIndex}`;
+ /** @type {HTMLSelectElement} */ (this._profileActiveSelect).value = `${profileIndex}`;
this._profileCurrent = profileIndex;
const profileEntry = this._getProfileEntry(profileIndex);
@@ -117,6 +155,10 @@ export class ProfileController {
await this._settingsController.setGlobalSetting('profileCurrent', profileIndex);
}
+ /**
+ * @param {number} sourceProfileIndex
+ * @param {number} destinationProfileIndex
+ */
async copyProfile(sourceProfileIndex, destinationProfileIndex) {
const sourceProfile = this._getProfile(sourceProfileIndex);
if (sourceProfile === null || !this._getProfile(destinationProfileIndex)) { return; }
@@ -140,9 +182,12 @@ export class ProfileController {
await this._settingsController.refresh();
}
+ /**
+ * @param {number} profileIndex
+ */
async duplicateProfile(profileIndex) {
const profile = this._getProfile(profileIndex);
- if (this.profile === null) { return; }
+ if (profile === null) { return; }
// Create new profile
const newProfile = clone(profile);
@@ -169,6 +214,9 @@ export class ProfileController {
this._settingsController.profileIndex = index;
}
+ /**
+ * @param {number} profileIndex
+ */
async deleteProfile(profileIndex) {
const profile = this._getProfile(profileIndex);
if (profile === null || this.profileCount <= 1) { return; }
@@ -178,6 +226,7 @@ export class ProfileController {
const settingsProfileIndex = this._settingsController.profileIndex;
// Construct settings modifications
+ /** @type {import('settings-modifications').Modification[]} */
const modifications = [{
action: 'splice',
path: 'profiles',
@@ -225,6 +274,10 @@ export class ProfileController {
await this._settingsController.modifyGlobalSettings(modifications);
}
+ /**
+ * @param {number} index1
+ * @param {number} index2
+ */
async swapProfiles(index1, index2) {
const profile1 = this._getProfile(index1);
const profile2 = this._getProfile(index2);
@@ -238,6 +291,7 @@ export class ProfileController {
const settingsProfileIndexNew = this._getSwappedValue(settingsProfileIndex, index1, index2);
// Construct settings modifications
+ /** @type {import('settings-modifications').Modification[]} */
const modifications = [{
action: 'swap',
path1: `profiles[${index1}]`,
@@ -278,15 +332,21 @@ export class ProfileController {
}
}
+ /**
+ * @param {number} profileIndex
+ */
openDeleteProfileModal(profileIndex) {
const profile = this._getProfile(profileIndex);
if (profile === null || this.profileCount <= 1) { return; }
- this._removeProfileNameElement.textContent = profile.name;
- this._profileRemoveModal.node.dataset.profileIndex = `${profileIndex}`;
- this._profileRemoveModal.setVisible(true);
+ /** @type {HTMLElement} */ (this._removeProfileNameElement).textContent = profile.name;
+ /** @type {import('./modal.js').Modal} */ (this._profileRemoveModal).node.dataset.profileIndex = `${profileIndex}`;
+ /** @type {import('./modal.js').Modal} */ (this._profileRemoveModal).setVisible(true);
}
+ /**
+ * @param {number} profileIndex
+ */
openCopyProfileModal(profileIndex) {
const profile = this._getProfile(profileIndex);
if (profile === null || this.profileCount <= 1) { return; }
@@ -301,16 +361,20 @@ export class ProfileController {
}
const profileIndexString = `${profileIndex}`;
- for (const option of this._profileCopySourceSelect.querySelectorAll('option')) {
+ const select = /** @type {HTMLSelectElement} */ (this._profileCopySourceSelect);
+ for (const option of select.querySelectorAll('option')) {
const {value} = option;
option.disabled = (value === profileIndexString);
}
- this._profileCopySourceSelect.value = `${copyFromIndex}`;
+ select.value = `${copyFromIndex}`;
- this._profileCopyModal.node.dataset.profileIndex = `${profileIndex}`;
- this._profileCopyModal.setVisible(true);
+ /** @type {import('./modal.js').Modal} */ (this._profileCopyModal).node.dataset.profileIndex = `${profileIndex}`;
+ /** @type {import('./modal.js').Modal} */ (this._profileCopyModal).setVisible(true);
}
+ /**
+ * @param {number} profileIndex
+ */
openProfileConditionsModal(profileIndex) {
const profile = this._getProfile(profileIndex);
if (profile === null) { return; }
@@ -328,6 +392,7 @@ export class ProfileController {
// Private
+ /** */
async _onOptionsChanged() {
// Update state
const {profiles, profileCurrent} = await this._settingsController.getOptionsFull();
@@ -339,8 +404,8 @@ export class ProfileController {
// Udpate UI
this._updateProfileSelectOptions();
- this._profileActiveSelect.value = `${profileCurrent}`;
- this._profileTargetSelect.value = `${settingsProfileIndex}`;
+ /** @type {HTMLSelectElement} */ (this._profileActiveSelect).value = `${profileCurrent}`;
+ /** @type {HTMLSelectElement} */ (this._profileTargetSelect).value = `${settingsProfileIndex}`;
// Update profile conditions
this._profileConditionsUI.cleanup();
@@ -361,51 +426,65 @@ export class ProfileController {
}
}
+ /**
+ * @param {Event} e
+ */
_onProfileActiveChange(e) {
- const value = this._tryGetValidProfileIndex(e.currentTarget.value);
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const value = this._tryGetValidProfileIndex(element.value);
if (value === null) { return; }
this.setDefaultProfile(value);
}
+ /**
+ * @param {Event} e
+ */
_onProfileTargetChange(e) {
- const value = this._tryGetValidProfileIndex(e.currentTarget.value);
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const value = this._tryGetValidProfileIndex(element.value);
if (value === null) { return; }
this._settingsController.profileIndex = value;
}
+ /** */
_onAdd() {
this.duplicateProfile(this._settingsController.profileIndex);
}
+ /** */
_onDeleteConfirm() {
- const modal = this._profileRemoveModal;
+ const modal = /** @type {import('./modal.js').Modal} */ (this._profileRemoveModal);
modal.setVisible(false);
const {node} = modal;
- let profileIndex = node.dataset.profileIndex;
+ const profileIndex = node.dataset.profileIndex;
delete node.dataset.profileIndex;
- profileIndex = this._tryGetValidProfileIndex(profileIndex);
- if (profileIndex === null) { return; }
+ const validProfileIndex = this._tryGetValidProfileIndex(profileIndex);
+ if (validProfileIndex === null) { return; }
- this.deleteProfile(profileIndex);
+ this.deleteProfile(validProfileIndex);
}
+ /** */
_onCopyConfirm() {
- const modal = this._profileCopyModal;
+ const modal = /** @type {import('./modal.js').Modal} */ (this._profileCopyModal);
modal.setVisible(false);
const {node} = modal;
- let destinationProfileIndex = node.dataset.profileIndex;
+ const destinationProfileIndex = node.dataset.profileIndex;
delete node.dataset.profileIndex;
- destinationProfileIndex = this._tryGetValidProfileIndex(destinationProfileIndex);
- if (destinationProfileIndex === null) { return; }
+ const validDestinationProfileIndex = this._tryGetValidProfileIndex(destinationProfileIndex);
+ if (validDestinationProfileIndex === null) { return; }
- const sourceProfileIndex = this._tryGetValidProfileIndex(this._profileCopySourceSelect.value);
+ const sourceProfileIndex = this._tryGetValidProfileIndex(/** @type {HTMLSelectElement} */ (this._profileCopySourceSelect).value);
if (sourceProfileIndex === null) { return; }
- this.copyProfile(sourceProfileIndex, destinationProfileIndex);
+ this.copyProfile(sourceProfileIndex, validDestinationProfileIndex);
}
+ /**
+ * @param {import('profile-conditions-ui').ConditionGroupCountChangedEvent} details
+ */
_onConditionGroupCountChanged({count, profileIndex}) {
if (profileIndex >= 0 && profileIndex < this._profileEntryList.length) {
const profileEntry = this._profileEntryList[profileIndex];
@@ -413,15 +492,19 @@ export class ProfileController {
}
}
+ /**
+ * @param {number} profileIndex
+ */
_addProfileEntry(profileIndex) {
const profile = this._profiles[profileIndex];
- const node = this._settingsController.instantiateTemplate('profile-entry');
- const entry = new ProfileEntry(this, node);
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('profile-entry'));
+ const entry = new ProfileEntry(this, node, profile, profileIndex);
this._profileEntryList.push(entry);
- entry.prepare(profile, profileIndex);
- this._profileEntryListContainer.appendChild(node);
+ entry.prepare();
+ /** @type {HTMLElement} */ (this._profileEntryListContainer).appendChild(node);
}
+ /** */
_updateProfileSelectOptions() {
for (const select of this._getAllProfileSelects()) {
const fragment = document.createDocumentFragment();
@@ -437,6 +520,10 @@ export class ProfileController {
}
}
+ /**
+ * @param {number} index
+ * @param {string} name
+ */
_updateSelectName(index, name) {
const optionValue = `${index}`;
for (const select of this._getAllProfileSelects()) {
@@ -448,14 +535,21 @@ export class ProfileController {
}
}
+ /**
+ * @returns {HTMLSelectElement[]}
+ */
_getAllProfileSelects() {
return [
- this._profileActiveSelect,
- this._profileTargetSelect,
- this._profileCopySourceSelect
+ /** @type {HTMLSelectElement} */ (this._profileActiveSelect),
+ /** @type {HTMLSelectElement} */ (this._profileTargetSelect),
+ /** @type {HTMLSelectElement} */ (this._profileCopySourceSelect)
];
}
+ /**
+ * @param {string|undefined} stringValue
+ * @returns {?number}
+ */
_tryGetValidProfileIndex(stringValue) {
if (typeof stringValue !== 'string') { return null; }
const intValue = parseInt(stringValue, 10);
@@ -467,6 +561,12 @@ export class ProfileController {
);
}
+ /**
+ * @param {string} name
+ * @param {import('settings').Profile[]} profiles
+ * @param {number} maxUniqueAttempts
+ * @returns {string}
+ */
_createCopyName(name, profiles, maxUniqueAttempts) {
let space, index, prefix, suffix;
const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name);
@@ -502,44 +602,80 @@ export class ProfileController {
}
}
+ /**
+ * @template T
+ * @param {T} currentValue
+ * @param {T} value1
+ * @param {T} value2
+ * @returns {T}
+ */
_getSwappedValue(currentValue, value1, value2) {
if (currentValue === value1) { return value2; }
if (currentValue === value2) { return value1; }
return currentValue;
}
+ /**
+ * @param {number} profileIndex
+ * @returns {?import('settings').Profile}
+ */
_getProfile(profileIndex) {
return (profileIndex >= 0 && profileIndex < this._profiles.length ? this._profiles[profileIndex] : null);
}
+ /**
+ * @param {number} profileIndex
+ * @returns {?ProfileEntry}
+ */
_getProfileEntry(profileIndex) {
return (profileIndex >= 0 && profileIndex < this._profileEntryList.length ? this._profileEntryList[profileIndex] : null);
}
+ /**
+ * @param {Element} node1
+ * @param {Element} node2
+ */
_swapDomNodes(node1, node2) {
const parent1 = node1.parentNode;
const parent2 = node2.parentNode;
const next1 = node1.nextSibling;
const next2 = node2.nextSibling;
- if (node2 !== next1) { parent1.insertBefore(node2, next1); }
- if (node1 !== next2) { parent2.insertBefore(node1, next2); }
+ if (node2 !== next1 && parent1 !== null) { parent1.insertBefore(node2, next1); }
+ if (node1 !== next2 && parent2 !== null) { parent2.insertBefore(node1, next2); }
}
}
class ProfileEntry {
- constructor(profileController, node) {
+ /**
+ * @param {ProfileController} profileController
+ * @param {HTMLElement} node
+ * @param {import('settings').Profile} profile
+ * @param {number} index
+ */
+ constructor(profileController, node, profile, index) {
+ /** @type {ProfileController} */
this._profileController = profileController;
+ /** @type {HTMLElement} */
this._node = node;
- this._profile = null;
- this._index = 0;
- this._isDefaultRadio = null;
- this._nameInput = null;
- this._countLink = null;
- this._countText = null;
- this._menuButton = null;
+ /** @type {import('settings').Profile} */
+ this._profile = profile;
+ /** @type {number} */
+ this._index = index;
+ /** @type {HTMLInputElement} */
+ this._isDefaultRadio = /** @type {HTMLInputElement} */ (node.querySelector('.profile-entry-is-default-radio'));
+ /** @type {HTMLInputElement} */
+ this._nameInput = /** @type {HTMLInputElement} */ (node.querySelector('.profile-entry-name-input'));
+ /** @type {HTMLElement} */
+ this._countLink = /** @type {HTMLElement} */ (node.querySelector('.profile-entry-condition-count-link'));
+ /** @type {HTMLElement} */
+ this._countText = /** @type {HTMLElement} */ (node.querySelector('.profile-entry-condition-count'));
+ /** @type {HTMLButtonElement} */
+ this._menuButton = /** @type {HTMLButtonElement} */ (node.querySelector('.profile-entry-menu-button'));
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
}
+ /** @type {number} */
get index() {
return this._index;
}
@@ -548,21 +684,13 @@ class ProfileEntry {
this._index = value;
}
+ /** @type {HTMLElement} */
get node() {
return this._node;
}
- prepare(profile, index) {
- this._profile = profile;
- this._index = index;
-
- const node = this._node;
- this._isDefaultRadio = node.querySelector('.profile-entry-is-default-radio');
- this._nameInput = node.querySelector('.profile-entry-name-input');
- this._countLink = node.querySelector('.profile-entry-condition-count-link');
- this._countText = node.querySelector('.profile-entry-condition-count');
- this._menuButton = node.querySelector('.profile-entry-menu-button');
-
+ /** */
+ prepare() {
this.updateState();
this._eventListeners.addEventListener(this._isDefaultRadio, 'change', this._onIsDefaultRadioChange.bind(this), false);
@@ -572,6 +700,7 @@ class ProfileEntry {
this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
if (this._node.parentNode !== null) {
@@ -579,41 +708,63 @@ class ProfileEntry {
}
}
+ /**
+ * @param {string} value
+ */
setName(value) {
if (this._nameInput.value === value) { return; }
this._nameInput.value = value;
}
+ /**
+ * @param {boolean} value
+ */
setIsDefault(value) {
this._isDefaultRadio.checked = value;
}
+ /** */
updateState() {
this._nameInput.value = this._profile.name;
this._countText.textContent = `${this._profile.conditionGroups.length}`;
this._isDefaultRadio.checked = (this._index === this._profileController.profileCurrentIndex);
}
+ /**
+ * @param {number} count
+ */
setConditionGroupsCount(count) {
this._countText.textContent = `${count}`;
}
// Private
+ /**
+ * @param {Event} e
+ */
_onIsDefaultRadioChange(e) {
- if (!e.currentTarget.checked) { return; }
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ if (!element.checked) { return; }
this._profileController.setDefaultProfile(this._index);
}
+ /**
+ * @param {Event} e
+ */
_onNameInputInput(e) {
- const name = e.currentTarget.value;
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const name = element.value;
this._profileController.setProfileName(this._index, name);
}
+ /** */
_onConditionsCountLinkClick() {
this._profileController.openProfileConditionsModal(this._index);
}
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const bodyNode = e.detail.menu.bodyNode;
const count = this._profileController.profileCount;
@@ -623,6 +774,9 @@ class ProfileEntry {
this._setMenuActionEnabled(bodyNode, 'delete', count > 1);
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'moveUp':
@@ -646,8 +800,13 @@ class ProfileEntry {
}
}
+ /**
+ * @param {Element} menu
+ * @param {string} action
+ * @param {boolean} enabled
+ */
_setMenuActionEnabled(menu, action, enabled) {
- const element = menu.querySelector(`[data-menu-action="${action}"]`);
+ const element = /** @type {HTMLButtonElement} */ (menu.querySelector(`[data-menu-action="${action}"]`));
if (element === null) { return; }
element.disabled = !enabled;
}
diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js
index e04dbdf7..b19311aa 100644
--- a/ext/js/pages/settings/recommended-permissions-controller.js
+++ b/ext/js/pages/settings/recommended-permissions-controller.js
@@ -19,13 +19,21 @@
import {EventListenerCollection} from '../../core.js';
export class RecommendedPermissionsController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?NodeListOf<HTMLInputElement>} */
this._originToggleNodes = null;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?HTMLElement} */
this._errorContainer = null;
}
+ /** */
async prepare() {
this._originToggleNodes = document.querySelectorAll('.recommended-permissions-toggle');
this._errorContainer = document.querySelector('#recommended-permissions-error');
@@ -39,35 +47,53 @@ export class RecommendedPermissionsController {
// Private
+ /**
+ * @param {import('settings-controller').PermissionsChangedEvent} details
+ */
_onPermissionsChanged({permissions}) {
this._eventListeners.removeAllEventListeners();
const originsSet = new Set(permissions.origins);
- for (const node of this._originToggleNodes) {
- node.checked = originsSet.has(node.dataset.origin);
+ if (this._originToggleNodes !== null) {
+ for (const node of this._originToggleNodes) {
+ const {origin} = node.dataset;
+ node.checked = typeof origin === 'string' && originsSet.has(origin);
+ }
}
}
+ /**
+ * @param {Event} e
+ */
_onOriginToggleChange(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
const value = node.checked;
node.checked = !value;
const {origin} = node.dataset;
+ if (typeof origin !== 'string') { return; }
this._setOriginPermissionEnabled(origin, value);
}
+ /** */
async _updatePermissions() {
const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
this._onPermissionsChanged({permissions});
}
+ /**
+ * @param {string} origin
+ * @param {boolean} enabled
+ * @returns {Promise<boolean>}
+ */
async _setOriginPermissionEnabled(origin, enabled) {
let added = false;
try {
added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled);
} catch (e) {
- this._errorContainer.hidden = false;
- this._errorContainer.textContent = e.message;
+ if (this._errorContainer !== null) {
+ this._errorContainer.hidden = false;
+ this._errorContainer.textContent = e instanceof Error ? e.message : `${e}`;
+ }
}
if (!added) { return false; }
await this._updatePermissions();
diff --git a/ext/js/pages/settings/scan-inputs-controller.js b/ext/js/pages/settings/scan-inputs-controller.js
index 252e7238..53423bdc 100644
--- a/ext/js/pages/settings/scan-inputs-controller.js
+++ b/ext/js/pages/settings/scan-inputs-controller.js
@@ -17,26 +17,37 @@
*/
import {EventListenerCollection} from '../../core.js';
+import {DocumentUtil} from '../../dom/document-util.js';
import {yomitan} from '../../yomitan.js';
import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';
export class ScanInputsController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?import('environment').OperatingSystem} */
this._os = null;
+ /** @type {?HTMLElement} */
this._container = null;
+ /** @type {?HTMLButtonElement} */
this._addButton = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._scanningInputCountNodes = null;
+ /** @type {ScanInputField[]} */
this._entries = [];
}
+ /** */
async prepare() {
const {platform: {os}} = await yomitan.api.getEnvironmentInfo();
this._os = os;
- this._container = document.querySelector('#scan-input-list');
- this._addButton = document.querySelector('#scan-input-add');
- this._scanningInputCountNodes = document.querySelectorAll('.scanning-input-count');
+ this._container = /** @type {HTMLElement} */ (document.querySelector('#scan-input-list'));
+ this._addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#scan-input-add'));
+ this._scanningInputCountNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.scanning-input-count'));
this._addButton.addEventListener('click', this._onAddButtonClick.bind(this), false);
this._settingsController.on('scanInputsChanged', this._onScanInputsChanged.bind(this));
@@ -45,6 +56,10 @@ export class ScanInputsController {
this.refresh();
}
+ /**
+ * @param {number} index
+ * @returns {boolean}
+ */
removeInput(index) {
if (index < 0 || index >= this._entries.length) { return false; }
const input = this._entries[index];
@@ -64,6 +79,12 @@ export class ScanInputsController {
return true;
}
+ /**
+ * @param {number} index
+ * @param {string} property
+ * @param {unknown} value
+ * @param {boolean} event
+ */
async setProperty(index, property, value, event) {
const path = `scanning.inputs[${index}].${property}`;
await this._settingsController.setProfileSetting(path, value);
@@ -72,22 +93,34 @@ export class ScanInputsController {
}
}
+ /**
+ * @param {string} name
+ * @returns {Element}
+ */
instantiateTemplate(name) {
return this._settingsController.instantiateTemplate(name);
}
+ /** */
async refresh() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
// Private
+ /**
+ * @param {import('settings-controller').ScanInputsChangedEvent} details
+ */
_onScanInputsChanged({source}) {
if (source === this) { return; }
this.refresh();
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
const {inputs} = options.scanning;
@@ -103,6 +136,9 @@ export class ScanInputsController {
this._updateCounts();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onAddButtonClick(e) {
e.preventDefault();
@@ -119,34 +155,51 @@ export class ScanInputsController {
}]);
// Scroll to bottom
- const button = e.currentTarget;
- const modalContainer = button.closest('.modal');
- const scrollContainer = modalContainer.querySelector('.modal-body');
+ const button = /** @type {HTMLElement} */ (e.currentTarget);
+ const modalContainer = /** @type {HTMLElement} */ (button.closest('.modal'));
+ const scrollContainer = /** @type {HTMLElement} */ (modalContainer.querySelector('.modal-body'));
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
+ /**
+ * @param {number} index
+ * @param {import('settings').ScanningInput} scanningInput
+ */
_addOption(index, scanningInput) {
+ if (this._os === null || this._container === null) { return; }
const field = new ScanInputField(this, index, this._os);
this._entries.push(field);
field.prepare(this._container, scanningInput);
}
+ /** */
_updateCounts() {
const stringValue = `${this._entries.length}`;
- for (const node of this._scanningInputCountNodes) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._scanningInputCountNodes)) {
node.textContent = stringValue;
}
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ */
async _modifyProfileSettings(targets) {
await this._settingsController.modifyProfileSettings(targets);
this._triggerScanInputsChanged();
}
+ /** */
_triggerScanInputsChanged() {
- this._settingsController.trigger('scanInputsChanged', {source: this});
+ /** @type {import('settings-controller').ScanInputsChangedEvent} */
+ const event = {source: this};
+ this._settingsController.trigger('scanInputsChanged', event);
}
+ /**
+ * @param {string} include
+ * @param {string} exclude
+ * @returns {import('settings').ScanningInput}
+ */
static createDefaultMouseInput(include, exclude) {
return {
include,
@@ -172,16 +225,29 @@ export class ScanInputsController {
}
class ScanInputField {
+ /**
+ * @param {ScanInputsController} parent
+ * @param {number} index
+ * @param {import('environment').OperatingSystem} os
+ */
constructor(parent, index, os) {
+ /** @type {ScanInputsController} */
this._parent = parent;
+ /** @type {number} */
this._index = index;
+ /** @type {import('environment').OperatingSystem} */
this._os = os;
+ /** @type {?HTMLElement} */
this._node = null;
+ /** @type {?KeyboardMouseInputField} */
this._includeInputField = null;
+ /** @type {?KeyboardMouseInputField} */
this._excludeInputField = null;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
}
+ /** @type {number} */
get index() {
return this._index;
}
@@ -191,16 +257,20 @@ class ScanInputField {
this._updateDataSettingTargets();
}
+ /**
+ * @param {HTMLElement} container
+ * @param {import('settings').ScanningInput} scanningInput
+ */
prepare(container, scanningInput) {
const {include, exclude, options: {showAdvanced}} = scanningInput;
- const node = this._parent.instantiateTemplate('scan-input');
- const includeInputNode = node.querySelector('.scan-input-field[data-property=include]');
- const includeMouseButton = node.querySelector('.mouse-button[data-property=include]');
- const excludeInputNode = node.querySelector('.scan-input-field[data-property=exclude]');
- const excludeMouseButton = node.querySelector('.mouse-button[data-property=exclude]');
- const removeButton = node.querySelector('.scan-input-remove');
- const menuButton = node.querySelector('.scanning-input-menu-button');
+ 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'));
node.dataset.showAdvanced = `${showAdvanced}`;
@@ -226,6 +296,7 @@ class ScanInputField {
this._updateDataSettingTargets();
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
if (this._includeInputField !== null) {
@@ -241,26 +312,38 @@ class ScanInputField {
// Private
+ /**
+ * @param {import('keyboard-mouse-input-field').ChangeEvent} details
+ */
_onIncludeValueChange({modifiers}) {
- modifiers = this._joinModifiers(modifiers);
- this._parent.setProperty(this._index, 'include', modifiers, true);
+ const modifiers2 = this._joinModifiers(modifiers);
+ this._parent.setProperty(this._index, 'include', modifiers2, true);
}
+ /**
+ * @param {import('keyboard-mouse-input-field').ChangeEvent} details
+ */
_onExcludeValueChange({modifiers}) {
- modifiers = this._joinModifiers(modifiers);
- this._parent.setProperty(this._index, 'exclude', modifiers, true);
+ const modifiers2 = this._joinModifiers(modifiers);
+ this._parent.setProperty(this._index, 'exclude', modifiers2, true);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onRemoveClick(e) {
e.preventDefault();
this._removeSelf();
}
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const bodyNode = e.detail.menu.bodyNode;
- const showAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="showAdvanced"]');
- const hideAdvanced = bodyNode.querySelector('.popup-menu-item[data-menu-action="hideAdvanced"]');
- const advancedVisible = (this._node.dataset.showAdvanced === 'true');
+ 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"]'));
+ const advancedVisible = (this._node !== null && this._node.dataset.showAdvanced === 'true');
if (showAdvanced !== null) {
showAdvanced.hidden = advancedVisible;
}
@@ -269,6 +352,9 @@ class ScanInputField {
}
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'remove':
@@ -281,40 +367,67 @@ class ScanInputField {
this._setAdvancedOptionsVisible(false);
break;
case 'clearInputs':
- this._includeInputField.clearInputs();
- this._excludeInputField.clearInputs();
+ /** @type {KeyboardMouseInputField} */ (this._includeInputField).clearInputs();
+ /** @type {KeyboardMouseInputField} */ (this._excludeInputField).clearInputs();
break;
}
}
+ /**
+ * @param {string} pointerType
+ * @returns {boolean}
+ */
_isPointerTypeSupported(pointerType) {
if (this._node === null) { return false; }
- const node = this._node.querySelector(`input.scan-input-settings-checkbox[data-property="types.${pointerType}"]`);
+ const node = /** @type {?HTMLInputElement} */ (this._node.querySelector(`input.scan-input-settings-checkbox[data-property="types.${pointerType}"]`));
return node !== null && node.checked;
}
+ /** */
_updateDataSettingTargets() {
+ if (this._node === null) { return; }
const index = this._index;
- for (const typeCheckbox of this._node.querySelectorAll('.scan-input-settings-checkbox')) {
+ for (const typeCheckbox of /** @type {NodeListOf<HTMLElement>} */ (this._node.querySelectorAll('.scan-input-settings-checkbox'))) {
const {property} = typeCheckbox.dataset;
typeCheckbox.dataset.setting = `scanning.inputs[${index}].${property}`;
}
}
+ /** */
_removeSelf() {
this._parent.removeInput(this._index);
}
+ /**
+ * @param {boolean} showAdvanced
+ */
_setAdvancedOptionsVisible(showAdvanced) {
showAdvanced = !!showAdvanced;
- this._node.dataset.showAdvanced = `${showAdvanced}`;
+ if (this._node !== null) {
+ this._node.dataset.showAdvanced = `${showAdvanced}`;
+ }
this._parent.setProperty(this._index, 'options.showAdvanced', showAdvanced, false);
}
+ /**
+ * @param {string} modifiersString
+ * @returns {import('input').Modifier[]}
+ */
_splitModifiers(modifiersString) {
- return modifiersString.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
+ /** @type {import('input').Modifier[]} */
+ const results = [];
+ for (const modifier of modifiersString.split(/[,;\s]+/)) {
+ const modifier2 = DocumentUtil.normalizeModifier(modifier.trim().toLowerCase());
+ if (modifier2 === null) { continue; }
+ results.push(modifier2);
+ }
+ return results;
}
+ /**
+ * @param {import('input').Modifier[]} modifiersArray
+ * @returns {string}
+ */
_joinModifiers(modifiersArray) {
return modifiersArray.join(', ');
}
diff --git a/ext/js/pages/settings/scan-inputs-simple-controller.js b/ext/js/pages/settings/scan-inputs-simple-controller.js
index 112c03a9..8d52af61 100644
--- a/ext/js/pages/settings/scan-inputs-simple-controller.js
+++ b/ext/js/pages/settings/scan-inputs-simple-controller.js
@@ -21,17 +21,26 @@ import {yomitan} from '../../yomitan.js';
import {ScanInputsController} from './scan-inputs-controller.js';
export class ScanInputsSimpleController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?HTMLInputElement} */
this._middleMouseButtonScan = null;
+ /** @type {?HTMLSelectElement} */
this._mainScanModifierKeyInput = null;
+ /** @type {boolean} */
this._mainScanModifierKeyInputHasOther = false;
+ /** @type {HotkeyUtil} */
this._hotkeyUtil = new HotkeyUtil();
}
+ /** */
async prepare() {
- this._middleMouseButtonScan = document.querySelector('#middle-mouse-button-scan');
- this._mainScanModifierKeyInput = document.querySelector('#main-scan-modifier-key');
+ 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;
@@ -40,27 +49,36 @@ export class ScanInputsSimpleController {
this._populateSelect(this._mainScanModifierKeyInput, this._mainScanModifierKeyInputHasOther);
const options = await this._settingsController.getOptions();
+ const optionsContext = this._settingsController.getOptionsContext();
this._middleMouseButtonScan.addEventListener('change', this.onMiddleMouseButtonScanChange.bind(this), false);
this._mainScanModifierKeyInput.addEventListener('change', this._onMainScanModifierKeyInputChange.bind(this), false);
this._settingsController.on('scanInputsChanged', this._onScanInputsChanged.bind(this));
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
- this._onOptionsChanged({options});
+ this._onOptionsChanged({options, optionsContext});
}
+ /** */
async refresh() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
// Private
+ /**
+ * @param {import('settings-controller').ScanInputsChangedEvent} details
+ */
_onScanInputsChanged({source}) {
if (source === this) { return; }
this.refresh();
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
const {scanning: {inputs}} = options;
const middleMouseSupportedIndex = this._getIndexOfMiddleMouseButtonScanInput(inputs);
@@ -87,27 +105,39 @@ export class ScanInputsSimpleController {
this._setHasMainScanInput(hasMainScanInput);
- this._middleMouseButtonScan.checked = middleMouseSupported;
- this._mainScanModifierKeyInput.value = mainScanInput;
+ /** @type {HTMLInputElement} */ (this._middleMouseButtonScan).checked = middleMouseSupported;
+ /** @type {HTMLSelectElement} */ (this._mainScanModifierKeyInput).value = mainScanInput;
}
+ /**
+ * @param {Event} e
+ */
onMiddleMouseButtonScanChange(e) {
- const middleMouseSupported = e.currentTarget.checked;
+ const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+ const middleMouseSupported = element.checked;
this._setMiddleMouseSuppported(middleMouseSupported);
}
+ /**
+ * @param {Event} e
+ */
_onMainScanModifierKeyInputChange(e) {
- const mainScanKey = e.currentTarget.value;
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ const mainScanKey = element.value;
if (mainScanKey === 'other') { return; }
const mainScanInputs = (mainScanKey === 'none' ? [] : [mainScanKey]);
this._setMainScanInputs(mainScanInputs);
}
+ /**
+ * @param {HTMLSelectElement} select
+ * @param {boolean} hasOther
+ */
_populateSelect(select, hasOther) {
const modifierKeys = [
{value: 'none', name: 'No key'}
];
- for (const value of ['alt', 'ctrl', 'shift', 'meta']) {
+ for (const value of /** @type {import('input').ModifierKey[]} */ (['alt', 'ctrl', 'shift', 'meta'])) {
const name = this._hotkeyUtil.getModifierDisplayValue(value);
modifierKeys.push({value, name});
}
@@ -127,10 +157,17 @@ export class ScanInputsSimpleController {
select.appendChild(fragment);
}
+ /**
+ * @param {string} value
+ * @returns {string[]}
+ */
_splitValue(value) {
return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
}
+ /**
+ * @param {boolean} value
+ */
async _setMiddleMouseSuppported(value) {
// Find target index
const options = await this._settingsController.getOptions();
@@ -163,8 +200,11 @@ export class ScanInputsSimpleController {
}
}
+ /**
+ * @param {string[]} value
+ */
async _setMainScanInputs(value) {
- value = value.join(', ');
+ const value2 = value.join(', ');
// Find target index
const options = await this._settingsController.getOptions();
@@ -175,7 +215,7 @@ export class ScanInputsSimpleController {
if (index < 0) {
// Add new
- const input = ScanInputsController.createDefaultMouseInput(value, 'mouse0');
+ const input = ScanInputsController.createDefaultMouseInput(value2, 'mouse0');
await this._modifyProfileSettings([{
action: 'splice',
path: 'scanning.inputs',
@@ -188,16 +228,25 @@ export class ScanInputsSimpleController {
await this._modifyProfileSettings([{
action: 'set',
path: `scanning.inputs[${index}].include`,
- value
+ value: value2
}]);
}
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ */
async _modifyProfileSettings(targets) {
await this._settingsController.modifyProfileSettings(targets);
- this._settingsController.trigger('scanInputsChanged', {source: this});
+ /** @type {import('settings-controller').ScanInputsChangedEvent} */
+ const event = {source: this};
+ this._settingsController.trigger('scanInputsChanged', event);
}
+ /**
+ * @param {import('settings').ScanningInput[]} inputs
+ * @returns {number}
+ */
_getIndexOfMainScanInput(inputs) {
for (let i = 0, ii = inputs.length; i < ii; ++i) {
const {include, exclude, types: {mouse}} = inputs[i];
@@ -218,6 +267,10 @@ export class ScanInputsSimpleController {
return -1;
}
+ /**
+ * @param {import('settings').ScanningInput[]} inputs
+ * @returns {number}
+ */
_getIndexOfMiddleMouseButtonScanInput(inputs) {
for (let i = 0, ii = inputs.length; i < ii; ++i) {
const {include, exclude, types: {mouse}} = inputs[i];
@@ -234,13 +287,22 @@ export class ScanInputsSimpleController {
return -1;
}
+ /**
+ * @param {string} input
+ * @returns {boolean}
+ */
_isMouseInput(input) {
return /^mouse\d+$/.test(input);
}
+ /**
+ * @param {boolean} hasMainScanInput
+ */
_setHasMainScanInput(hasMainScanInput) {
if (this._mainScanModifierKeyInputHasOther !== hasMainScanInput) { return; }
this._mainScanModifierKeyInputHasOther = !hasMainScanInput;
- this._populateSelect(this._mainScanModifierKeyInput, this._mainScanModifierKeyInputHasOther);
+ if (this._mainScanModifierKeyInput !== null) {
+ this._populateSelect(this._mainScanModifierKeyInput, this._mainScanModifierKeyInputHasOther);
+ }
}
}
diff --git a/ext/js/pages/settings/secondary-search-dictionary-controller.js b/ext/js/pages/settings/secondary-search-dictionary-controller.js
index cc873901..b20bd475 100644
--- a/ext/js/pages/settings/secondary-search-dictionary-controller.js
+++ b/ext/js/pages/settings/secondary-search-dictionary-controller.js
@@ -20,16 +20,25 @@ import {EventListenerCollection} from '../../core.js';
import {yomitan} from '../../yomitan.js';
export class SecondarySearchDictionaryController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?import('core').TokenObject} */
this._getDictionaryInfoToken = null;
+ /** @type {Map<string, import('dictionary-importer').Summary>} */
this._dictionaryInfoMap = new Map();
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?HTMLElement} */
this._container = null;
}
+ /** */
async prepare() {
- this._container = document.querySelector('#secondary-search-dictionary-list');
+ this._container = /** @type {HTMLElement} */ (document.querySelector('#secondary-search-dictionary-list'));
await this._onDatabaseUpdated();
@@ -40,7 +49,9 @@ export class SecondarySearchDictionaryController {
// Private
+ /** */
async _onDatabaseUpdated() {
+ /** @type {?import('core').TokenObject} */
const token = {};
this._getDictionaryInfoToken = token;
const dictionaries = await this._settingsController.getDictionaryInfo();
@@ -52,10 +63,12 @@ export class SecondarySearchDictionaryController {
this._dictionaryInfoMap.set(entry.title, entry);
}
- const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ await this._onDictionarySettingsReordered();
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
this._eventListeners.removeAllEventListeners();
@@ -67,31 +80,38 @@ export class SecondarySearchDictionaryController {
const dictionaryInfo = this._dictionaryInfoMap.get(name);
if (typeof dictionaryInfo === 'undefined') { continue; }
- const node = this._settingsController.instantiateTemplate('secondary-search-dictionary');
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('secondary-search-dictionary'));
fragment.appendChild(node);
- const nameNode = node.querySelector('.dictionary-title');
+ const nameNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-title'));
nameNode.textContent = name;
- const versionNode = node.querySelector('.dictionary-version');
+ const versionNode = /** @type {HTMLElement} */ (node.querySelector('.dictionary-version'));
versionNode.textContent = `rev.${dictionaryInfo.revision}`;
- const toggle = node.querySelector('.dictionary-allow-secondary-searches');
+ const toggle = /** @type {HTMLElement} */ (node.querySelector('.dictionary-allow-secondary-searches'));
toggle.dataset.setting = `dictionaries[${i}].allowSecondarySearches`;
this._eventListeners.addEventListener(toggle, 'settingChanged', this._onEnabledChanged.bind(this, node), false);
}
- this._container.textContent = '';
- this._container.appendChild(fragment);
+ const container = /** @type {HTMLElement} */ (this._container);
+ container.textContent = '';
+ container.appendChild(fragment);
}
+ /**
+ * @param {HTMLElement} node
+ * @param {import('dom-data-binder').SettingChangedEvent} e
+ */
_onEnabledChanged(node, e) {
const {detail: {value}} = e;
node.dataset.enabled = `${value}`;
}
+ /** */
async _onDictionarySettingsReordered() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
}
diff --git a/ext/js/pages/settings/sentence-termination-characters-controller.js b/ext/js/pages/settings/sentence-termination-characters-controller.js
index f901b82c..80c4cdbe 100644
--- a/ext/js/pages/settings/sentence-termination-characters-controller.js
+++ b/ext/js/pages/settings/sentence-termination-characters-controller.js
@@ -19,26 +19,38 @@
import {EventListenerCollection} from '../../core.js';
export class SentenceTerminationCharactersController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
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 {import('./settings-controller.js').SettingsController} */
get settingsController() {
return this._settingsController;
}
+ /** */
async prepare() {
- this._addButton = document.querySelector('#sentence-termination-character-list-add');
- this._resetButton = document.querySelector('#sentence-termination-character-list-reset');
- this._listTable = document.querySelector('#sentence-termination-character-list-table');
- this._listContainer = document.querySelector('#sentence-termination-character-list');
- this._emptyIndicator = document.querySelector('#sentence-termination-character-list-empty');
+ 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));
@@ -47,6 +59,9 @@ export class SentenceTerminationCharactersController {
await this._updateOptions();
}
+ /**
+ * @param {import('settings').SentenceParsingTerminationCharacterOption} terminationCharacterEntry
+ */
async addEntry(terminationCharacterEntry) {
const options = await this._settingsController.getOptions();
const {sentenceParsing: {terminationCharacters}} = options;
@@ -62,6 +77,10 @@ export class SentenceTerminationCharactersController {
await this._updateOptions();
}
+ /**
+ * @param {number} index
+ * @returns {Promise<boolean>}
+ */
async deleteEntry(index) {
const options = await this._settingsController.getOptions();
const {sentenceParsing: {terminationCharacters}} = options;
@@ -80,12 +99,19 @@ export class SentenceTerminationCharactersController {
return true;
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async modifyProfileSettings(targets) {
return await this._settingsController.modifyProfileSettings(targets);
}
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
for (const entry of this._entries) {
entry.cleanup();
@@ -94,29 +120,37 @@ export class SentenceTerminationCharactersController {
this._entries = [];
const {sentenceParsing: {terminationCharacters}} = options;
+ const listContainer = /** @type {HTMLElement} */ (this._listContainer);
for (let i = 0, ii = terminationCharacters.length; i < ii; ++i) {
const terminationCharacterEntry = terminationCharacters[i];
- const node = this._settingsController.instantiateTemplate('sentence-termination-character-entry');
- this._listContainer.appendChild(node);
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('sentence-termination-character-entry'));
+ listContainer.appendChild(node);
const entry = new SentenceTerminationCharacterEntry(this, terminationCharacterEntry, i, node);
this._entries.push(entry);
entry.prepare();
}
- this._listTable.hidden = (terminationCharacters.length === 0);
- this._emptyIndicator.hidden = (terminationCharacters.length !== 0);
+ /** @type {HTMLElement} */ (this._listTable).hidden = (terminationCharacters.length === 0);
+ /** @type {HTMLElement} */ (this._emptyIndicator).hidden = (terminationCharacters.length !== 0);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onAddClick(e) {
e.preventDefault();
this._addNewEntry();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onResetClick(e) {
e.preventDefault();
this._reset();
}
+ /** */
async _addNewEntry() {
const newEntry = {
enabled: true,
@@ -125,14 +159,17 @@ export class SentenceTerminationCharactersController {
includeCharacterAtStart: false,
includeCharacterAtEnd: false
};
- return await this.addEntry(newEntry);
+ await this.addEntry(newEntry);
}
+ /** */
async _updateOptions() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /** */
async _reset() {
const defaultOptions = await this._settingsController.getDefaultOptions();
const value = defaultOptions.profiles[0].options.sentenceParsing.terminationCharacters;
@@ -142,28 +179,43 @@ export class SentenceTerminationCharactersController {
}
class SentenceTerminationCharacterEntry {
+ /**
+ * @param {SentenceTerminationCharactersController} parent
+ * @param {import('settings').SentenceParsingTerminationCharacterOption} data
+ * @param {number} index
+ * @param {HTMLElement} node
+ */
constructor(parent, data, index, node) {
+ /** @type {SentenceTerminationCharactersController} */
this._parent = parent;
+ /** @type {import('settings').SentenceParsingTerminationCharacterOption} */
this._data = data;
+ /** @type {number} */
this._index = index;
+ /** @type {HTMLElement} */
this._node = node;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?HTMLInputElement} */
this._character1Input = null;
+ /** @type {?HTMLInputElement} */
this._character2Input = null;
+ /** @type {string} */
this._basePath = `sentenceParsing.terminationCharacters[${this._index}]`;
}
+ /** */
prepare() {
const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} = this._data;
const node = this._node;
- const enabledToggle = node.querySelector('.sentence-termination-character-enabled');
- const typeSelect = node.querySelector('.sentence-termination-character-type');
- const character1Input = node.querySelector('.sentence-termination-character-input1');
- const character2Input = node.querySelector('.sentence-termination-character-input2');
- const includeAtStartCheckbox = node.querySelector('.sentence-termination-character-include-at-start');
- const includeAtEndheckbox = node.querySelector('.sentence-termination-character-include-at-end');
- const menuButton = node.querySelector('.sentence-termination-character-entry-button');
+ 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'));
this._character1Input = character1Input;
this._character2Input = character2Input;
@@ -188,6 +240,7 @@ class SentenceTerminationCharacterEntry {
this._eventListeners.addEventListener(menuButton, 'menuClose', this._onMenuClose.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
if (this._node.parentNode !== null) {
@@ -197,12 +250,20 @@ class SentenceTerminationCharacterEntry {
// Private
+ /**
+ * @param {Event} e
+ */
_onTypeSelectChange(e) {
- this._setHasCharacter2(e.currentTarget.value === 'quote');
+ const element = /** @type {HTMLSelectElement} */ (e.currentTarget);
+ this._setHasCharacter2(element.value === 'quote');
}
+ /**
+ * @param {1|2} characterNumber
+ * @param {Event} e
+ */
_onCharacterChange(characterNumber, e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLInputElement} */ (e.currentTarget);
if (characterNumber === 2 && this._data.character2 === null) {
node.value = '';
}
@@ -211,6 +272,9 @@ class SentenceTerminationCharacterEntry {
this._setCharacterValue(node, characterNumber, value);
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'delete':
@@ -219,11 +283,16 @@ class SentenceTerminationCharacterEntry {
}
}
+ /** */
async _delete() {
this._parent.deleteEntry(this._index);
}
+ /**
+ * @param {boolean} has
+ */
async _setHasCharacter2(has) {
+ if (this._character2Input === null) { return; }
const okay = await this._setCharacterValue(this._character2Input, 2, has ? this._data.character1 : null);
if (okay) {
const type = (!has ? 'terminator' : 'quote');
@@ -231,14 +300,24 @@ class SentenceTerminationCharacterEntry {
}
}
+ /**
+ * @param {HTMLInputElement} inputNode
+ * @param {1|2} characterNumber
+ * @param {?string} value
+ * @returns {Promise<boolean>}
+ */
async _setCharacterValue(inputNode, characterNumber, value) {
- const pathEnd = `character${characterNumber}`;
- const r = await this._parent.settingsController.setProfileSetting(`${this._basePath}.${pathEnd}`, value);
+ if (characterNumber === 1 && typeof value !== 'string') { value = ''; }
+ const r = await this._parent.settingsController.setProfileSetting(`${this._basePath}.character${characterNumber}`, value);
const okay = !r[0].error;
if (okay) {
- this._data[pathEnd] = value;
+ if (characterNumber === 1) {
+ this._data.character1 = /** @type {string} */ (value);
+ } else {
+ this._data.character2 = value;
+ }
} else {
- value = this._data[pathEnd];
+ value = characterNumber === 1 ? this._data.character1 : this._data.character2;
}
inputNode.value = (value !== null ? value : '');
return okay;
diff --git a/ext/js/pages/settings/settings-controller.js b/ext/js/pages/settings/settings-controller.js
index 83ccdb39..1b46c745 100644
--- a/ext/js/pages/settings/settings-controller.js
+++ b/ext/js/pages/settings/settings-controller.js
@@ -22,21 +22,33 @@ import {PermissionsUtil} from '../../data/permissions-util.js';
import {HtmlTemplateCollection} from '../../dom/html-template-collection.js';
import {yomitan} from '../../yomitan.js';
+/**
+ * @augments EventDispatcher<import('settings-controller').EventType>
+ */
export class SettingsController extends EventDispatcher {
constructor() {
super();
+ /** @type {number} */
this._profileIndex = 0;
+ /** @type {string} */
this._source = generateId(16);
+ /** @type {Set<import('settings-controller').PageExitPrevention>} */
this._pageExitPreventions = new Set();
+ /** @type {EventListenerCollection} */
this._pageExitPreventionEventListeners = new EventListenerCollection();
- this._templates = new HtmlTemplateCollection(document);
+ /** @type {HtmlTemplateCollection} */
+ this._templates = new HtmlTemplateCollection();
+ this._templates.load(document);
+ /** @type {PermissionsUtil} */
this._permissionsUtil = new PermissionsUtil();
}
+ /** @type {string} */
get source() {
return this._source;
}
+ /** @type {number} */
get profileIndex() {
return this._profileIndex;
}
@@ -46,10 +58,12 @@ export class SettingsController extends EventDispatcher {
this._setProfileIndex(value, true);
}
+ /** @type {PermissionsUtil} */
get permissionsUtil() {
return this._permissionsUtil;
}
+ /** */
async prepare() {
yomitan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
if (this._canObservePermissionsChanges()) {
@@ -63,67 +77,121 @@ export class SettingsController extends EventDispatcher {
}
}
+ /** */
async refresh() {
await this._onOptionsUpdatedInternal(true);
}
+ /**
+ * @returns {Promise<import('settings').ProfileOptions>}
+ */
async getOptions() {
const optionsContext = this.getOptionsContext();
return await yomitan.api.optionsGet(optionsContext);
}
+ /**
+ * @returns {Promise<import('settings').Options>}
+ */
async getOptionsFull() {
return await yomitan.api.optionsGetFull();
}
+ /**
+ * @param {import('settings').Options} value
+ */
async setAllSettings(value) {
const profileIndex = value.profileCurrent;
await yomitan.api.setAllSettings(value, this._source);
this._setProfileIndex(profileIndex, true);
}
+ /**
+ * @param {import('settings-modifications').ScopedRead[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async getSettings(targets) {
- return await this._getSettings(targets, {});
+ return await this._getSettings(targets, null);
}
+ /**
+ * @param {import('settings-modifications').Read[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async getGlobalSettings(targets) {
- return await this._getSettings(targets, {scope: 'global'});
+ return await this._getSettings(targets, {scope: 'global', optionsContext: null});
}
+ /**
+ * @param {import('settings-modifications').Read[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async getProfileSettings(targets) {
- return await this._getSettings(targets, {scope: 'profile'});
+ return await this._getSettings(targets, {scope: 'profile', optionsContext: null});
}
+ /**
+ * @param {import('settings-modifications').ScopedModification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async modifySettings(targets) {
- return await this._modifySettings(targets, {});
+ return await this._modifySettings(targets, null);
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async modifyGlobalSettings(targets) {
- return await this._modifySettings(targets, {scope: 'global'});
+ return await this._modifySettings(targets, {scope: 'global', optionsContext: null});
}
+ /**
+ * @param {import('settings-modifications').Modification[]} targets
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async modifyProfileSettings(targets) {
- return await this._modifySettings(targets, {scope: 'profile'});
+ return await this._modifySettings(targets, {scope: 'profile', optionsContext: null});
}
+ /**
+ * @param {string} path
+ * @param {unknown} value
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async setGlobalSetting(path, value) {
return await this.modifyGlobalSettings([{action: 'set', path, value}]);
}
+ /**
+ * @param {string} path
+ * @param {unknown} value
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async setProfileSetting(path, value) {
return await this.modifyProfileSettings([{action: 'set', path, value}]);
}
+ /**
+ * @returns {Promise<import('dictionary-importer').Summary[]>}
+ */
async getDictionaryInfo() {
return await yomitan.api.getDictionaryInfo();
}
+ /**
+ * @returns {import('settings').OptionsContext}
+ */
getOptionsContext() {
return {index: this._profileIndex};
}
+ /**
+ * @returns {import('settings-controller').PageExitPrevention}
+ */
preventPageExit() {
- const obj = {end: null};
+ /** @type {import('settings-controller').PageExitPrevention} */
+ const obj = {};
obj.end = this._endPreventPageExit.bind(this, obj);
if (this._pageExitPreventionEventListeners.size === 0) {
this._pageExitPreventionEventListeners.addEventListener(window, 'beforeunload', this._onBeforeUnload.bind(this), false);
@@ -132,14 +200,25 @@ export class SettingsController extends EventDispatcher {
return obj;
}
+ /**
+ * @param {string} name
+ * @returns {Element}
+ */
instantiateTemplate(name) {
return this._templates.instantiate(name);
}
+ /**
+ * @param {string} name
+ * @returns {DocumentFragment}
+ */
instantiateTemplateFragment(name) {
return this._templates.instantiateFragment(name);
}
+ /**
+ * @returns {Promise<import('settings').Options>}
+ */
async getDefaultOptions() {
const optionsUtil = new OptionsUtil();
await optionsUtil.prepare();
@@ -149,22 +228,34 @@ export class SettingsController extends EventDispatcher {
// Private
+ /**
+ * @param {number} value
+ * @param {boolean} canUpdateProfileIndex
+ */
_setProfileIndex(value, canUpdateProfileIndex) {
this._profileIndex = value;
this.trigger('optionsContextChanged');
this._onOptionsUpdatedInternal(canUpdateProfileIndex);
}
+ /**
+ * @param {{source: string}} details
+ */
_onOptionsUpdated({source}) {
if (source === this._source) { return; }
this._onOptionsUpdatedInternal(true);
}
+ /**
+ * @param {boolean} canUpdateProfileIndex
+ */
async _onOptionsUpdatedInternal(canUpdateProfileIndex) {
const optionsContext = this.getOptionsContext();
try {
const options = await this.getOptions();
- this.trigger('optionsChanged', {options, optionsContext});
+ /** @type {import('settings-controller').OptionsChangedEvent} */
+ const event = {options, optionsContext};
+ this.trigger('optionsChanged', event);
} catch (e) {
if (canUpdateProfileIndex) {
this._setProfileIndex(0, false);
@@ -174,26 +265,49 @@ export class SettingsController extends EventDispatcher {
}
}
- _setupTargets(targets, extraFields) {
- return targets.map((target) => {
- target = Object.assign({}, extraFields, target);
- if (target.scope === 'profile') {
- target.optionsContext = this.getOptionsContext();
- }
- return target;
- });
+ /**
+ * @param {import('settings-modifications').OptionsScope} target
+ */
+ _modifyOptionsScope(target) {
+ if (target.scope === 'profile') {
+ target.optionsContext = this.getOptionsContext();
+ }
}
+ /**
+ * @template {boolean} THasScope
+ * @param {import('settings-controller').SettingsRead<THasScope>[]} targets
+ * @param {import('settings-controller').SettingsExtraFields<THasScope>} extraFields
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async _getSettings(targets, extraFields) {
- targets = this._setupTargets(targets, extraFields);
- return await yomitan.api.getSettings(targets);
+ const targets2 = targets.map((target) => {
+ const target2 = /** @type {import('settings-controller').SettingsRead<true>} */ (Object.assign({}, extraFields, target));
+ this._modifyOptionsScope(target2);
+ return target2;
+ });
+ return await yomitan.api.getSettings(targets2);
}
+ /**
+ * @template {boolean} THasScope
+ * @param {import('settings-controller').SettingsModification<THasScope>[]} targets
+ * @param {import('settings-controller').SettingsExtraFields<THasScope>} extraFields
+ * @returns {Promise<import('settings-controller').ModifyResult[]>}
+ */
async _modifySettings(targets, extraFields) {
- targets = this._setupTargets(targets, extraFields);
- return await yomitan.api.modifySettings(targets, this._source);
+ const targets2 = targets.map((target) => {
+ const target2 = /** @type {import('settings-controller').SettingsModification<true>} */ (Object.assign({}, extraFields, target));
+ this._modifyOptionsScope(target2);
+ return target2;
+ });
+ return await yomitan.api.modifySettings(targets2, this._source);
}
+ /**
+ * @param {BeforeUnloadEvent} e
+ * @returns {string|undefined}
+ */
_onBeforeUnload(e) {
if (this._pageExitPreventions.size === 0) {
return;
@@ -204,6 +318,9 @@ export class SettingsController extends EventDispatcher {
return '';
}
+ /**
+ * @param {import('settings-controller').PageExitPrevention} obj
+ */
_endPreventPageExit(obj) {
this._pageExitPreventions.delete(obj);
if (this._pageExitPreventions.size === 0) {
@@ -211,18 +328,25 @@ export class SettingsController extends EventDispatcher {
}
}
+ /** */
_onPermissionsChanged() {
this._triggerPermissionsChanged();
}
+ /** */
async _triggerPermissionsChanged() {
- const event = 'permissionsChanged';
- if (!this.hasListeners(event)) { return; }
+ const eventName = 'permissionsChanged';
+ if (!this.hasListeners(eventName)) { return; }
const permissions = await this._permissionsUtil.getAllPermissions();
- this.trigger(event, {permissions});
+ /** @type {import('settings-controller').PermissionsChangedEvent} */
+ const event = {permissions};
+ this.trigger(eventName, event);
}
+ /**
+ * @returns {boolean}
+ */
_canObservePermissionsChanges() {
return isObject(chrome.permissions) && isObject(chrome.permissions.onAdded) && isObject(chrome.permissions.onRemoved);
}
diff --git a/ext/js/pages/settings/settings-display-controller.js b/ext/js/pages/settings/settings-display-controller.js
index e23e355d..16e6cfae 100644
--- a/ext/js/pages/settings/settings-display-controller.js
+++ b/ext/js/pages/settings/settings-display-controller.js
@@ -21,44 +21,54 @@ import {PopupMenu} from '../../dom/popup-menu.js';
import {SelectorObserver} from '../../dom/selector-observer.js';
export class SettingsDisplayController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ * @param {import('./modal-controller.js').ModalController} modalController
+ */
constructor(settingsController, modalController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {import('./modal-controller.js').ModalController} */
this._modalController = modalController;
+ /** @type {?HTMLElement} */
this._contentNode = null;
+ /** @type {?HTMLElement} */
this._menuContainer = null;
- this._onMoreToggleClickBind = null;
- this._onMenuButtonClickBind = null;
+ /** @type {(event: MouseEvent) => void} */
+ this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this);
+ /** @type {(event: MouseEvent) => void} */
+ this._onMenuButtonClickBind = this._onMenuButtonClick.bind(this);
}
+ /** */
prepare() {
- this._contentNode = document.querySelector('.content');
- this._menuContainer = document.querySelector('#popup-menus');
+ 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 document.querySelectorAll('.fab-button')) {
+ for (const fabButton of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.fab-button'))) {
fabButton.addEventListener('click', onFabButtonClick, false);
}
const onModalAction = this._onModalAction.bind(this);
- for (const node of document.querySelectorAll('[data-modal-action]')) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('[data-modal-action]'))) {
node.addEventListener('click', onModalAction, false);
}
const onSelectOnClickElementClick = this._onSelectOnClickElementClick.bind(this);
- for (const node of document.querySelectorAll('[data-select-on-click]')) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('[data-select-on-click]'))) {
node.addEventListener('click', onSelectOnClickElementClick, false);
}
const onInputTabActionKeyDown = this._onInputTabActionKeyDown.bind(this);
- for (const node of document.querySelectorAll('[data-tab-action]')) {
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('[data-tab-action]'))) {
node.addEventListener('keydown', onInputTabActionKeyDown, false);
}
- for (const node of document.querySelectorAll('.defer-load-iframe')) {
+ for (const node of /** @type {NodeListOf<HTMLIFrameElement>} */ (document.querySelectorAll('.defer-load-iframe'))) {
this._setupDeferLoadIframe(node);
}
- this._onMoreToggleClickBind = this._onMoreToggleClick.bind(this);
const moreSelectorObserver = new SelectorObserver({
selector: '.more-toggle',
onAdded: this._onMoreSetup.bind(this),
@@ -66,7 +76,6 @@ export class SettingsDisplayController {
});
moreSelectorObserver.observe(document.documentElement, false);
- this._onMenuButtonClickBind = this._onMenuButtonClick.bind(this);
const menuSelectorObserver = new SelectorObserver({
selector: '[data-menu]',
onAdded: this._onMenuSetup.bind(this),
@@ -81,32 +90,54 @@ export class SettingsDisplayController {
// Private
+ /**
+ * @param {Element} element
+ * @returns {null}
+ */
_onMoreSetup(element) {
- element.addEventListener('click', this._onMoreToggleClickBind, false);
+ /** @type {HTMLElement} */ (element).addEventListener('click', this._onMoreToggleClickBind, false);
return null;
}
+ /**
+ * @param {Element} element
+ */
_onMoreCleanup(element) {
- element.removeEventListener('click', this._onMoreToggleClickBind, false);
+ /** @type {HTMLElement} */ (element).removeEventListener('click', this._onMoreToggleClickBind, false);
}
+ /**
+ * @param {Element} element
+ * @returns {null}
+ */
_onMenuSetup(element) {
- element.addEventListener('click', this._onMenuButtonClickBind, false);
+ /** @type {HTMLElement} */ (element).addEventListener('click', this._onMenuButtonClickBind, false);
return null;
}
+ /**
+ * @param {Element} element
+ */
_onMenuCleanup(element) {
- element.removeEventListener('click', this._onMenuButtonClickBind, false);
+ /** @type {HTMLElement} */ (element).removeEventListener('click', this._onMenuButtonClickBind, false);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onMenuButtonClick(e) {
- const element = e.currentTarget;
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
const {menu} = element.dataset;
+ if (typeof menu === 'undefined') { return; }
this._showMenu(element, menu);
}
+ /**
+ * @param {MouseEvent} e
+ */
_onFabButtonClick(e) {
- const action = e.currentTarget.dataset.action;
+ const element = /** @type {HTMLElement} */ (e.currentTarget);
+ const action = element.dataset.action;
switch (action) {
case 'toggle-sidebar':
document.body.classList.toggle('sidebar-visible');
@@ -117,16 +148,20 @@ export class SettingsDisplayController {
}
}
+ /**
+ * @param {MouseEvent} e
+ */
_onMoreToggleClick(e) {
- const container = this._getMoreContainer(e.currentTarget);
+ const node = /** @type {HTMLElement} */ (e.currentTarget);
+ const container = this._getMoreContainer(node);
if (container === null) { return; }
- const more = container.querySelector('.more');
+ const more = /** @type {?HTMLElement} */ (container.querySelector('.more'));
if (more === null) { return; }
const moreVisible = more.hidden;
more.hidden = !moreVisible;
- for (const moreToggle of container.querySelectorAll('.more-toggle')) {
+ for (const moreToggle of /** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll('.more-toggle'))) {
const container2 = this._getMoreContainer(moreToggle);
if (container2 === null) { continue; }
@@ -137,13 +172,16 @@ export class SettingsDisplayController {
}
e.preventDefault();
- return false;
}
+ /** */
_onPopState() {
this._updateScrollTarget();
}
+ /**
+ * @param {KeyboardEvent} e
+ */
_onKeyDown(e) {
switch (e.code) {
case 'Escape':
@@ -155,12 +193,18 @@ export class SettingsDisplayController {
}
}
+ /**
+ * @param {MouseEvent} e
+ */
_onModalAction(e) {
- const node = e.currentTarget;
+ const node = /** @type {HTMLElement} */ (e.currentTarget);
const {modalAction} = node.dataset;
if (typeof modalAction !== 'string') { return; }
- let [action, target] = modalAction.split(',');
+ const modalActionArray = modalAction.split(',');
+ const action = modalActionArray[0];
+ /** @type {string|Element|undefined} */
+ let target = modalActionArray[1];
if (typeof target === 'undefined') {
const currentModal = node.closest('.modal');
if (currentModal === null) { return; }
@@ -185,26 +229,33 @@ export class SettingsDisplayController {
e.preventDefault();
}
+ /**
+ * @param {MouseEvent} e
+ */
_onSelectOnClickElementClick(e) {
if (e.button !== 0) { return; }
- const node = e.currentTarget;
+ const node = /** @type {HTMLElement} */ (e.currentTarget);
const range = document.createRange();
range.selectNode(node);
const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
+ if (selection !== null) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
e.preventDefault();
e.stopPropagation();
- return false;
}
+ /**
+ * @param {KeyboardEvent} e
+ */
_onInputTabActionKeyDown(e) {
if (e.key !== 'Tab' || e.ctrlKey) { return; }
- const node = e.currentTarget;
+ const node = /** @type {HTMLElement} */ (e.currentTarget);
const {tabAction} = node.dataset;
if (typeof tabAction !== 'string') { return; }
@@ -220,6 +271,7 @@ export class SettingsDisplayController {
}
}
+ /** */
_updateScrollTarget() {
const hash = window.location.hash;
if (!hash.startsWith('#!')) { return; }
@@ -233,18 +285,25 @@ export class SettingsDisplayController {
content.scrollTop += rect2.top - rect1.top;
}
+ /**
+ * @param {HTMLElement} link
+ * @returns {?Element}
+ */
_getMoreContainer(link) {
const v = link.dataset.parentDistance;
const distance = v ? parseInt(v, 10) : 1;
if (Number.isNaN(distance)) { return null; }
+ /** @type {?Element} */
+ let result = link;
for (let i = 0; i < distance; ++i) {
- link = link.parentNode;
- if (link === null) { break; }
+ if (result === null) { break; }
+ result = /** @type {?Element} */ (result.parentNode);
}
- return link;
+ return result;
}
+ /** */
_closeTopMenuOrModal() {
for (const popupMenu of PopupMenu.openMenus) {
popupMenu.close();
@@ -257,17 +316,27 @@ export class SettingsDisplayController {
}
}
+ /**
+ * @param {HTMLElement} element
+ * @param {string} menuName
+ */
_showMenu(element, menuName) {
- const menu = this._settingsController.instantiateTemplate(menuName);
- if (menu === null) { return; }
+ const menu = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate(menuName));
- this._menuContainer.appendChild(menu);
+ /** @type {HTMLElement} */ (this._menuContainer).appendChild(menu);
const popupMenu = new PopupMenu(element, menu);
popupMenu.prepare();
}
+ /**
+ * @param {KeyboardEvent} e
+ * @param {HTMLElement} node
+ * @param {string[]} args
+ */
_indentInput(e, node, args) {
+ if (!(node instanceof HTMLTextAreaElement)) { return; }
+
let indent = '\t';
if (args.length > 1) {
const count = parseInt(args[1], 10);
@@ -276,7 +345,8 @@ export class SettingsDisplayController {
const {selectionStart: start, selectionEnd: end, value} = node;
const lineStart = value.substring(0, start).lastIndexOf('\n') + 1;
- const lineWhitespace = /^[ \t]*/.exec(value.substring(lineStart))[0];
+ const lineWhitespaceMatch = /^[ \t]*/.exec(value.substring(lineStart));
+ const lineWhitespace = lineWhitespaceMatch !== null ? lineWhitespaceMatch[0] : '';
if (e.shiftKey) {
const whitespaceLength = Math.max(0, Math.floor((lineWhitespace.length - 1) / 4) * 4);
@@ -298,17 +368,23 @@ export class SettingsDisplayController {
}
}
+ /**
+ * @param {HTMLIFrameElement} element
+ */
_setupDeferLoadIframe(element) {
const parent = this._getMoreContainer(element);
if (parent === null) { return; }
+ /** @type {?MutationObserver} */
let mutationObserver = null;
const callback = () => {
if (!this._isElementVisible(element)) { return false; }
const src = element.dataset.src;
delete element.dataset.src;
- element.src = src;
+ if (typeof src === 'string') {
+ element.src = src;
+ }
if (mutationObserver === null) { return true; }
@@ -323,6 +399,10 @@ export class SettingsDisplayController {
mutationObserver.observe(parent, {attributes: true});
}
+ /**
+ * @param {HTMLElement} element
+ * @returns {boolean}
+ */
_isElementVisible(element) {
return (element.offsetParent !== null);
}
diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js
index f9ea0aa1..1b9723e8 100644
--- a/ext/js/pages/settings/settings-main.js
+++ b/ext/js/pages/settings/settings-main.js
@@ -49,6 +49,9 @@ import {StatusFooter} from './status-footer.js';
import {StorageController} from './storage-controller.js';
import {TranslationTextReplacementsController} from './translation-text-replacements-controller.js';
+/**
+ * @param {GenericSettingController} genericSettingController
+ */
async function setupGenericSettingsController(genericSettingController) {
await genericSettingController.prepare();
await genericSettingController.refresh();
@@ -62,10 +65,11 @@ async function setupGenericSettingsController(genericSettingController) {
const extensionContentController = new ExtensionContentController();
extensionContentController.prepare();
- const statusFooter = new StatusFooter(document.querySelector('.status-footer-container'));
+ const statusFooter = new StatusFooter(/** @type {HTMLElement} */ (document.querySelector('.status-footer-container')));
statusFooter.prepare();
- let prepareTimer = setTimeout(() => {
+ /** @type {?number} */
+ let prepareTimer = window.setTimeout(() => {
prepareTimer = null;
document.documentElement.dataset.loadingStalled = 'true';
}, 1000);
diff --git a/ext/js/pages/settings/sort-frequency-dictionary-controller.js b/ext/js/pages/settings/sort-frequency-dictionary-controller.js
index 53104085..e7759d95 100644
--- a/ext/js/pages/settings/sort-frequency-dictionary-controller.js
+++ b/ext/js/pages/settings/sort-frequency-dictionary-controller.js
@@ -19,20 +19,30 @@
import {yomitan} from '../../yomitan.js';
export class SortFrequencyDictionaryController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
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 {?import('core').TokenObject} */
this._getDictionaryInfoToken = null;
}
+ /** */
async prepare() {
- this._sortFrequencyDictionarySelect = document.querySelector('#sort-frequency-dictionary');
- this._sortFrequencyDictionaryOrderSelect = document.querySelector('#sort-frequency-dictionary-order');
- this._sortFrequencyDictionaryOrderAutoButton = document.querySelector('#sort-frequency-dictionary-order-auto');
- this._sortFrequencyDictionaryOrderContainerNode = document.querySelector('#sort-frequency-dictionary-order-container');
+ 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();
@@ -45,7 +55,9 @@ export class SortFrequencyDictionaryController {
// Private
+ /** */
async _onDatabaseUpdated() {
+ /** @type {?import('core').TokenObject} */
const token = {};
this._getDictionaryInfoToken = token;
const dictionaries = await this._settingsController.getDictionaryInfo();
@@ -55,33 +67,44 @@ export class SortFrequencyDictionaryController {
this._updateDictionaryOptions(dictionaries);
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
const {sortFrequencyDictionary, sortFrequencyDictionaryOrder} = options.general;
- this._sortFrequencyDictionarySelect.value = (sortFrequencyDictionary !== null ? sortFrequencyDictionary : '');
- this._sortFrequencyDictionaryOrderSelect.value = sortFrequencyDictionaryOrder;
- this._sortFrequencyDictionaryOrderContainerNode.hidden = (sortFrequencyDictionary === null);
+ /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionarySelect).value = (sortFrequencyDictionary !== null ? sortFrequencyDictionary : '');
+ /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionaryOrderSelect).value = sortFrequencyDictionaryOrder;
+ /** @type {HTMLElement} */ (this._sortFrequencyDictionaryOrderContainerNode).hidden = (sortFrequencyDictionary === null);
}
+ /** */
_onSortFrequencyDictionarySelectChange() {
- let {value} = this._sortFrequencyDictionarySelect;
- if (value === '') { value = null; }
- this._setSortFrequencyDictionaryValue(value);
+ const {value} = /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionarySelect);
+ this._setSortFrequencyDictionaryValue(value !== '' ? value : null);
}
+ /** */
_onSortFrequencyDictionaryOrderSelectChange() {
- const {value} = this._sortFrequencyDictionaryOrderSelect;
- this._setSortFrequencyDictionaryOrderValue(value);
+ const {value} = /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionaryOrderSelect);
+ const value2 = this._normalizeSortFrequencyDictionaryOrder(value);
+ if (value2 === null) { return; }
+ this._setSortFrequencyDictionaryOrderValue(value2);
}
+ /** */
_onSortFrequencyDictionaryOrderAutoButtonClick() {
- const {value} = this._sortFrequencyDictionarySelect;
+ const {value} = /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionarySelect);
if (value === '') { return; }
this._autoUpdateOrder(value);
}
+ /**
+ * @param {import('dictionary-importer').Summary[]} dictionaries
+ */
_updateDictionaryOptions(dictionaries) {
const fragment = document.createDocumentFragment();
let option = document.createElement('option');
@@ -95,30 +118,44 @@ export class SortFrequencyDictionaryController {
option.textContent = title;
fragment.appendChild(option);
}
- this._sortFrequencyDictionarySelect.textContent = '';
- this._sortFrequencyDictionarySelect.appendChild(fragment);
+ const select = /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionarySelect);
+ select.textContent = '';
+ select.appendChild(fragment);
}
+ /**
+ * @param {?string} value
+ */
async _setSortFrequencyDictionaryValue(value) {
- this._sortFrequencyDictionaryOrderContainerNode.hidden = (value === null);
+ /** @type {HTMLElement} */ (this._sortFrequencyDictionaryOrderContainerNode).hidden = (value === null);
await this._settingsController.setProfileSetting('general.sortFrequencyDictionary', value);
if (value !== null) {
await this._autoUpdateOrder(value);
}
}
+ /**
+ * @param {import('settings').SortFrequencyDictionaryOrder} value
+ */
async _setSortFrequencyDictionaryOrderValue(value) {
await this._settingsController.setProfileSetting('general.sortFrequencyDictionaryOrder', value);
}
+ /**
+ * @param {string} dictionary
+ */
async _autoUpdateOrder(dictionary) {
const order = await this._getFrequencyOrder(dictionary);
if (order === 0) { return; }
const value = (order > 0 ? 'descending' : 'ascending');
- this._sortFrequencyDictionaryOrderSelect.value = value;
+ /** @type {HTMLSelectElement} */ (this._sortFrequencyDictionaryOrderSelect).value = value;
await this._setSortFrequencyDictionaryOrderValue(value);
}
+ /**
+ * @param {string} dictionary
+ * @returns {Promise<number>}
+ */
async _getFrequencyOrder(dictionary) {
const moreCommonTerms = ['来る', '言う', '出る', '入る', '方', '男', '女', '今', '何', '時'];
const lessCommonTerms = ['行なう', '論じる', '過す', '行方', '人口', '猫', '犬', '滝', '理', '暁'];
@@ -129,6 +166,7 @@ export class SortFrequencyDictionaryController {
[dictionary]
);
+ /** @type {Map<string, {hasValue: boolean, minValue: number, maxValue: number}>} */
const termDetails = new Map();
const moreCommonTermDetails = [];
const lessCommonTermDetails = [];
@@ -144,7 +182,6 @@ export class SortFrequencyDictionaryController {
}
for (const {term, frequency} of frequencies) {
- if (typeof frequency !== 'number') { continue; }
const details = termDetails.get(term);
if (typeof details === 'undefined') { continue; }
details.minValue = Math.min(details.minValue, frequency);
@@ -163,10 +200,28 @@ export class SortFrequencyDictionaryController {
return Math.sign(result);
}
+ /**
+ * @param {import('dictionary-importer').SummaryCounts} counts
+ * @returns {boolean}
+ */
_dictionaryHasNoFrequencies(counts) {
if (typeof counts !== 'object' || counts === null) { return false; }
const {termMeta} = counts;
if (typeof termMeta !== 'object' || termMeta === null) { return false; }
return termMeta.freq <= 0;
}
+
+ /**
+ * @param {string} value
+ * @returns {?import('settings').SortFrequencyDictionaryOrder}
+ */
+ _normalizeSortFrequencyDictionaryOrder(value) {
+ switch (value) {
+ case 'ascending':
+ case 'descending':
+ return value;
+ default:
+ return null;
+ }
+ }
}
diff --git a/ext/js/pages/settings/status-footer.js b/ext/js/pages/settings/status-footer.js
index 6c64794f..a8f1a8c4 100644
--- a/ext/js/pages/settings/status-footer.js
+++ b/ext/js/pages/settings/status-footer.js
@@ -19,34 +19,52 @@
import {PanelElement} from '../../dom/panel-element.js';
export class StatusFooter extends PanelElement {
+ /**
+ * @param {HTMLElement} node
+ */
constructor(node) {
super({
node,
closingAnimationDuration: 375 // Milliseconds; includes buffer
});
- this._body = node.querySelector('.status-footer');
+ /** @type {HTMLElement} */
+ this._body = /** @type {HTMLElement} */ (node.querySelector('.status-footer'));
}
+ /** */
prepare() {
- this.on('closeCompleted', this._onCloseCompleted.bind(this), false);
- this._body.querySelector('.status-footer-header-close').addEventListener('click', this._onCloseClick.bind(this), false);
+ const closeButton = /** @type {HTMLElement} */ (this._body.querySelector('.status-footer-header-close'));
+ this.on('closeCompleted', this._onCloseCompleted.bind(this));
+ closeButton.addEventListener('click', this._onCloseClick.bind(this), false);
}
+ /**
+ * @param {string} selector
+ * @returns {?HTMLElement}
+ */
getTaskContainer(selector) {
return this._body.querySelector(selector);
}
+ /**
+ * @param {string} selector
+ * @returns {boolean}
+ */
isTaskActive(selector) {
const target = this.getTaskContainer(selector);
- return (target !== null && target.dataset.active);
+ return (target !== null && !!target.dataset.active);
}
+ /**
+ * @param {string} selector
+ * @param {boolean} active
+ */
setTaskActive(selector, active) {
const target = this.getTaskContainer(selector);
if (target === null) { return; }
const activeElements = new Set();
- for (const element of this._body.querySelectorAll('.status-footer-item')) {
+ for (const element of /** @type {NodeListOf<HTMLElement>} */ (this._body.querySelectorAll('.status-footer-item'))) {
if (element.dataset.active) {
activeElements.add(element);
}
@@ -68,13 +86,17 @@ export class StatusFooter extends PanelElement {
// Private
+ /**
+ * @param {MouseEvent} e
+ */
_onCloseClick(e) {
e.preventDefault();
this.setVisible(false);
}
+ /** */
_onCloseCompleted() {
- for (const element of this._body.querySelectorAll('.status-footer-item')) {
+ for (const element of /** @type {NodeListOf<HTMLElement>} */ (this._body.querySelectorAll('.status-footer-item'))) {
if (!element.dataset.active) {
element.hidden = true;
}
diff --git a/ext/js/pages/settings/storage-controller.js b/ext/js/pages/settings/storage-controller.js
index ba1145b8..7f323b48 100644
--- a/ext/js/pages/settings/storage-controller.js
+++ b/ext/js/pages/settings/storage-controller.js
@@ -19,28 +19,43 @@
import {yomitan} from '../../yomitan.js';
export class StorageController {
+ /**
+ * @param {import('./persistent-storage-controller.js').PersistentStorageController} persistentStorageController
+ */
constructor(persistentStorageController) {
+ /** @type {import('./persistent-storage-controller.js').PersistentStorageController} */
this._persistentStorageController = persistentStorageController;
+ /** @type {?StorageEstimate} */
this._mostRecentStorageEstimate = null;
+ /** @type {boolean} */
this._storageEstimateFailed = false;
+ /** @type {boolean} */
this._isUpdating = false;
- this._storageUsageNode = null;
- this._storageQuotaNode = null;
+ /** @type {?NodeListOf<HTMLElement>} */
+ this._storageUsageNodes = null;
+ /** @type {?NodeListOf<HTMLElement>} */
+ this._storageQuotaNodes = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._storageUseFiniteNodes = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._storageUseInfiniteNodes = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._storageUseValidNodes = null;
+ /** @type {?NodeListOf<HTMLElement>} */
this._storageUseInvalidNodes = null;
}
+ /** */
prepare() {
- this._storageUsageNodes = document.querySelectorAll('.storage-usage');
- this._storageQuotaNodes = document.querySelectorAll('.storage-quota');
- this._storageUseFiniteNodes = document.querySelectorAll('.storage-use-finite');
- this._storageUseInfiniteNodes = document.querySelectorAll('.storage-use-infinite');
- this._storageUseValidNodes = document.querySelectorAll('.storage-use-valid');
- this._storageUseInvalidNodes = document.querySelectorAll('.storage-use-invalid');
-
- document.querySelector('#storage-refresh').addEventListener('click', this._onStorageRefreshButtonClick.bind(this), false);
+ this._storageUsageNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-usage'));
+ this._storageQuotaNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-quota'));
+ this._storageUseFiniteNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-use-finite'));
+ this._storageUseInfiniteNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-use-infinite'));
+ this._storageUseValidNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-use-valid'));
+ this._storageUseInvalidNodes = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.storage-use-invalid'));
+ const storageRefreshButton = /** @type {HTMLButtonElement} */ (document.querySelector('#storage-refresh'));
+
+ storageRefreshButton.addEventListener('click', this._onStorageRefreshButtonClick.bind(this), false);
yomitan.on('storageChanged', this._onStorageChanged.bind(this));
this._updateStats();
@@ -48,14 +63,17 @@ export class StorageController {
// Private
+ /** */
_onStorageRefreshButtonClick() {
this._updateStats();
}
+ /** */
_onStorageChanged() {
this._updateStats();
}
+ /** */
async _updateStats() {
if (this._isUpdating) { return; }
@@ -66,13 +84,18 @@ export class StorageController {
const valid = (estimate !== null);
// Firefox reports usage as 0 when persistent storage is enabled.
- const finite = valid && (estimate.usage > 0 || !(await this._persistentStorageController.isStoragePeristent()));
+ const finite = valid && ((typeof estimate.usage === 'number' && estimate.usage > 0) || !(await this._persistentStorageController.isStoragePeristent()));
if (finite) {
- for (const node of this._storageUsageNodes) {
- node.textContent = this._bytesToLabeledString(estimate.usage);
+ let {usage, quota} = estimate;
+ if (typeof usage !== 'number') { usage = 0; }
+ if (typeof quota !== 'number') { quota = 0; }
+ const usageString = this._bytesToLabeledString(usage);
+ const quotaString = this._bytesToLabeledString(quota);
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._storageUsageNodes)) {
+ node.textContent = usageString;
}
- for (const node of this._storageQuotaNodes) {
- node.textContent = this._bytesToLabeledString(estimate.quota);
+ for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._storageQuotaNodes)) {
+ node.textContent = quotaString;
}
}
@@ -80,8 +103,6 @@ export class StorageController {
this._setElementsVisible(this._storageUseInfiniteNodes, valid && !finite);
this._setElementsVisible(this._storageUseValidNodes, valid);
this._setElementsVisible(this._storageUseInvalidNodes, !valid);
-
- return valid;
} finally {
this._isUpdating = false;
}
@@ -89,6 +110,9 @@ export class StorageController {
// Private
+ /**
+ * @returns {Promise<?StorageEstimate>}
+ */
async _storageEstimate() {
if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) {
return null;
@@ -103,6 +127,10 @@ export class StorageController {
return null;
}
+ /**
+ * @param {number} size
+ * @returns {string}
+ */
_bytesToLabeledString(size) {
const base = 1000;
const labels = [' bytes', 'KB', 'MB', 'GB', 'TB'];
@@ -116,7 +144,12 @@ export class StorageController {
return `${label}${labels[labelIndex]}`;
}
+ /**
+ * @param {?NodeListOf<HTMLElement>} elements
+ * @param {boolean} visible
+ */
_setElementsVisible(elements, visible) {
+ if (elements === null) { return; }
visible = !visible;
for (const element of elements) {
element.hidden = visible;
diff --git a/ext/js/pages/settings/translation-text-replacements-controller.js b/ext/js/pages/settings/translation-text-replacements-controller.js
index 4a860b52..050db8d1 100644
--- a/ext/js/pages/settings/translation-text-replacements-controller.js
+++ b/ext/js/pages/settings/translation-text-replacements-controller.js
@@ -19,15 +19,22 @@
import {EventListenerCollection} from '../../core.js';
export class TranslationTextReplacementsController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
this._settingsController = settingsController;
+ /** @type {?HTMLElement} */
this._entryContainer = null;
+ /** @type {TranslationTextReplacementsEntry[]} */
this._entries = [];
}
+ /** */
async prepare() {
- this._entryContainer = document.querySelector('#translation-text-replacement-list');
- const addButton = document.querySelector('#translation-text-replacement-add');
+ this._entryContainer = /** @type {HTMLElement} */ (document.querySelector('#translation-text-replacement-list'));
+ const addButton = /** @type {HTMLButtonElement} */ (document.querySelector('#translation-text-replacement-add'));
addButton.addEventListener('click', this._onAdd.bind(this), false);
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
@@ -35,11 +42,12 @@ export class TranslationTextReplacementsController {
await this._updateOptions();
}
-
+ /** */
async addGroup() {
const options = await this._settingsController.getOptions();
const {groups} = options.translation.textReplacements;
const newEntry = this._createNewEntry();
+ /** @type {import('settings-modifications').Modification} */
const target = (
(groups.length === 0) ?
{
@@ -62,6 +70,10 @@ export class TranslationTextReplacementsController {
await this._updateOptions();
}
+ /**
+ * @param {number} index
+ * @returns {Promise<boolean>}
+ */
async deleteGroup(index) {
const options = await this._settingsController.getOptions();
const {groups} = options.translation.textReplacements;
@@ -70,6 +82,7 @@ export class TranslationTextReplacementsController {
const group0 = groups[0];
if (index < 0 || index >= group0.length) { return false; }
+ /** @type {import('settings-modifications').Modification} */
const target = (
(group0.length > 1) ?
{
@@ -95,6 +108,9 @@ export class TranslationTextReplacementsController {
// Private
+ /**
+ * @param {import('settings-controller').OptionsChangedEvent} details
+ */
_onOptionsChanged({options}) {
for (const entry of this._entries) {
entry.cleanup();
@@ -105,50 +121,70 @@ export class TranslationTextReplacementsController {
if (groups.length > 0) {
const group0 = groups[0];
for (let i = 0, ii = group0.length; i < ii; ++i) {
- const data = group0[i];
- const node = this._settingsController.instantiateTemplate('translation-text-replacement-entry');
- this._entryContainer.appendChild(node);
- const entry = new TranslationTextReplacementsEntry(this, node, i, data);
+ const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('translation-text-replacement-entry'));
+ /** @type {HTMLElement} */ (this._entryContainer).appendChild(node);
+ const entry = new TranslationTextReplacementsEntry(this, node, i);
this._entries.push(entry);
entry.prepare();
}
}
}
+ /** */
_onAdd() {
this.addGroup();
}
+ /** */
async _updateOptions() {
const options = await this._settingsController.getOptions();
- this._onOptionsChanged({options});
+ const optionsContext = this._settingsController.getOptionsContext();
+ this._onOptionsChanged({options, optionsContext});
}
+ /**
+ * @returns {import('settings').TranslationTextReplacementGroup}
+ */
_createNewEntry() {
return {pattern: '', ignoreCase: false, replacement: ''};
}
}
class TranslationTextReplacementsEntry {
+ /**
+ * @param {TranslationTextReplacementsController} parent
+ * @param {HTMLElement} node
+ * @param {number} index
+ */
constructor(parent, node, index) {
+ /** @type {TranslationTextReplacementsController} */
this._parent = parent;
+ /** @type {HTMLElement} */
this._node = node;
+ /** @type {number} */
this._index = index;
+ /** @type {EventListenerCollection} */
this._eventListeners = new EventListenerCollection();
+ /** @type {?HTMLInputElement} */
this._patternInput = null;
+ /** @type {?HTMLInputElement} */
this._replacementInput = null;
+ /** @type {?HTMLInputElement} */
this._ignoreCaseToggle = null;
+ /** @type {?HTMLInputElement} */
this._testInput = null;
+ /** @type {?HTMLInputElement} */
this._testOutput = null;
}
+ /** */
prepare() {
- const patternInput = this._node.querySelector('.translation-text-replacement-pattern');
- const replacementInput = this._node.querySelector('.translation-text-replacement-replacement');
- const ignoreCaseToggle = this._node.querySelector('.translation-text-replacement-pattern-ignore-case');
- const menuButton = this._node.querySelector('.translation-text-replacement-button');
- const testInput = this._node.querySelector('.translation-text-replacement-test-input');
- const testOutput = this._node.querySelector('.translation-text-replacement-test-output');
+ 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'));
this._patternInput = patternInput;
this._replacementInput = replacementInput;
@@ -169,6 +205,7 @@ class TranslationTextReplacementsEntry {
this._eventListeners.addEventListener(testInput, 'input', this._updateTestInput.bind(this), false);
}
+ /** */
cleanup() {
this._eventListeners.removeAllEventListeners();
if (this._node.parentNode !== null) {
@@ -178,13 +215,19 @@ class TranslationTextReplacementsEntry {
// Private
+ /**
+ * @param {import('popup-menu').MenuOpenEvent} e
+ */
_onMenuOpen(e) {
const bodyNode = e.detail.menu.bodyNode;
const testVisible = this._isTestVisible();
- bodyNode.querySelector('[data-menu-action=showTest]').hidden = testVisible;
- bodyNode.querySelector('[data-menu-action=hideTest]').hidden = !testVisible;
+ /** @type {HTMLElement} */ (bodyNode.querySelector('[data-menu-action=showTest]')).hidden = testVisible;
+ /** @type {HTMLElement} */ (bodyNode.querySelector('[data-menu-action=hideTest]')).hidden = !testVisible;
}
+ /**
+ * @param {import('popup-menu').MenuCloseEvent} e
+ */
_onMenuClose(e) {
switch (e.detail.action) {
case 'remove':
@@ -199,34 +242,58 @@ class TranslationTextReplacementsEntry {
}
}
+ /**
+ * @param {import('dom-data-binder').SettingChangedEvent} deatils
+ */
_onPatternChanged({detail: {value}}) {
this._validatePattern(value);
this._updateTestInput();
}
+ /**
+ * @param {unknown} value
+ */
_validatePattern(value) {
let okay = false;
try {
- new RegExp(value, 'g');
- okay = true;
+ if (typeof value === 'string') {
+ new RegExp(value, 'g');
+ okay = true;
+ }
} catch (e) {
// NOP
}
- this._patternInput.dataset.invalid = `${!okay}`;
+ if (this._patternInput !== null) {
+ this._patternInput.dataset.invalid = `${!okay}`;
+ }
}
+ /**
+ * @returns {boolean}
+ */
_isTestVisible() {
return this._node.dataset.testVisible === 'true';
}
+ /**
+ * @param {boolean} visible
+ */
_setTestVisible(visible) {
this._node.dataset.testVisible = `${visible}`;
this._updateTestInput();
}
+ /** */
_updateTestInput() {
- if (!this._isTestVisible()) { return; }
+ if (
+ !this._isTestVisible() ||
+ this._ignoreCaseToggle === null ||
+ this._patternInput === null ||
+ this._replacementInput === null ||
+ this._testInput === null ||
+ this._testOutput === null
+ ) { return; }
const ignoreCase = this._ignoreCaseToggle.checked;
const pattern = this._patternInput.value;
diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js
index b1438187..c034aae1 100644
--- a/ext/js/pages/welcome-main.js
+++ b/ext/js/pages/welcome-main.js
@@ -30,6 +30,7 @@ import {SettingsController} from './settings/settings-controller.js';
import {SettingsDisplayController} from './settings/settings-display-controller.js';
import {StatusFooter} from './settings/status-footer.js';
+/** */
async function setupEnvironmentInfo() {
const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
const {browser, platform} = await yomitan.api.getEnvironmentInfo();
@@ -38,6 +39,9 @@ async function setupEnvironmentInfo() {
document.documentElement.dataset.manifestVersion = `${manifestVersion}`;
}
+/**
+ * @param {GenericSettingController} genericSettingController
+ */
async function setupGenericSettingsController(genericSettingController) {
await genericSettingController.prepare();
await genericSettingController.refresh();
@@ -51,7 +55,7 @@ async function setupGenericSettingsController(genericSettingController) {
const extensionContentController = new ExtensionContentController();
extensionContentController.prepare();
- const statusFooter = new StatusFooter(document.querySelector('.status-footer-container'));
+ const statusFooter = new StatusFooter(/** @type {HTMLElement} */ (document.querySelector('.status-footer-container')));
statusFooter.prepare();
await yomitan.prepare();