summaryrefslogtreecommitdiff
path: root/ext/js/pages/settings/anki-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/pages/settings/anki-controller.js')
-rw-r--r--ext/js/pages/settings/anki-controller.js427
1 files changed, 347 insertions, 80 deletions
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) {