Anki controller refactor (#954)
* Simplify data transform for anki.enable setting * Refactor AnkiController * Implement marker link clicking * Request permissions for clipboard
This commit is contained in:
parent
9e9bd0dcf6
commit
defd7402cf
@ -331,17 +331,14 @@ input[type=checkbox].storage-button-checkbox {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-data-show-button {
|
#anki-error-message-details-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
.error-data-show-button:after {
|
|
||||||
content: "\2026";
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-data-container {
|
#anki-error-message-details {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
@ -17,26 +17,47 @@
|
|||||||
|
|
||||||
/* global
|
/* global
|
||||||
* AnkiConnect
|
* AnkiConnect
|
||||||
|
* ObjectPropertyAccessor
|
||||||
|
* SelectorObserver
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AnkiController {
|
class AnkiController {
|
||||||
constructor(settingsController) {
|
constructor(settingsController) {
|
||||||
this._ankiConnect = new AnkiConnect();
|
|
||||||
this._settingsController = settingsController;
|
this._settingsController = settingsController;
|
||||||
|
this._ankiConnect = new AnkiConnect();
|
||||||
|
this._selectorObserver = new SelectorObserver({
|
||||||
|
selector: '.anki-card',
|
||||||
|
ignoreSelector: null,
|
||||||
|
onAdded: this._createCardController.bind(this),
|
||||||
|
onRemoved: this._removeCardController.bind(this),
|
||||||
|
isStale: this._isCardControllerStale.bind(this)
|
||||||
|
});
|
||||||
|
this._fieldMarkersRequiringClipboardPermission = new Set([
|
||||||
|
'clipboard-image',
|
||||||
|
'clipboard-text'
|
||||||
|
]);
|
||||||
|
this._ankiOptions = null;
|
||||||
|
this._getAnkiDataPromise = null;
|
||||||
|
this._ankiErrorContainer = null;
|
||||||
|
this._ankiErrorMessageContainer = null;
|
||||||
|
this._ankiErrorMessageDetailsContainer = null;
|
||||||
|
this._ankiErrorMessageDetailsToggle = null;
|
||||||
|
this._ankiErrorInvalidResponseInfo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare() {
|
async prepare() {
|
||||||
for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) {
|
this._ankiErrorContainer = document.querySelector('#anki-error');
|
||||||
element.addEventListener('change', this._onFieldsChanged.bind(this), false);
|
this._ankiErrorMessageContainer = document.querySelector('#anki-error-message');
|
||||||
}
|
this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details');
|
||||||
|
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"]');
|
||||||
|
|
||||||
for (const element of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
|
this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false);
|
||||||
element.addEventListener('change', this._onModelChanged.bind(this), false);
|
if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); }
|
||||||
}
|
|
||||||
|
|
||||||
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
|
|
||||||
|
|
||||||
const options = await this._settingsController.getOptions();
|
const options = await this._settingsController.getOptions();
|
||||||
|
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
|
||||||
this._onOptionsChanged({options});
|
this._onOptionsChanged({options});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,91 +113,261 @@ class AnkiController {
|
|||||||
getFieldMarkersHtml(markers) {
|
getFieldMarkersHtml(markers) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const marker of markers) {
|
for (const marker of markers) {
|
||||||
const markerNode = this._settingsController.instantiateTemplate('anki-field-marker');
|
const markerNode = this._settingsController.instantiateTemplate('anki-card-field-marker');
|
||||||
markerNode.querySelector('.marker-link').textContent = marker;
|
markerNode.querySelector('.marker-link').textContent = marker;
|
||||||
fragment.appendChild(markerNode);
|
fragment.appendChild(markerNode);
|
||||||
}
|
}
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAnkiData() {
|
||||||
|
let promise = this._getAnkiDataPromise;
|
||||||
|
if (promise === null) {
|
||||||
|
promise = this._getAnkiData();
|
||||||
|
this._getAnkiDataPromise = promise;
|
||||||
|
promise.finally(() => { this._getAnkiDataPromise = null; });
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModelFieldNames(model) {
|
||||||
|
return await this._ankiConnect.getModelFieldNames(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFieldPermissions(fieldValue) {
|
||||||
|
let requireClipboard = false;
|
||||||
|
const markers = this._getFieldMarkers(fieldValue);
|
||||||
|
for (const marker of markers) {
|
||||||
|
if (this._fieldMarkersRequiringClipboardPermission.has(marker)) {
|
||||||
|
requireClipboard = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireClipboard) {
|
||||||
|
this._requestClipboardReadPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
async _onOptionsChanged({options}) {
|
async _onOptionsChanged({options: {anki}}) {
|
||||||
const {server, enable: enabled} = options.anki;
|
this._ankiOptions = anki;
|
||||||
this._ankiConnect.server = server;
|
this._ankiConnect.server = anki.server;
|
||||||
this._ankiConnect.enabled = enabled;
|
this._ankiConnect.enabled = anki.enable;
|
||||||
|
|
||||||
if (!enabled) { return; }
|
this._selectorObserver.disconnect();
|
||||||
|
this._selectorObserver.observe(document.documentElement, true);
|
||||||
|
}
|
||||||
|
|
||||||
await this._deckAndModelPopulate(options);
|
_onAnkiErrorMessageDetailsToggleClick() {
|
||||||
await Promise.all([
|
const node = this._ankiErrorMessageDetailsContainer;
|
||||||
this._populateFields('terms', options.anki.terms.fields),
|
node.hidden = !node.hidden;
|
||||||
this._populateFields('kanji', options.anki.kanji.fields)
|
}
|
||||||
|
|
||||||
|
_onAnkiEnableChanged({detail: {value}}) {
|
||||||
|
if (this._ankiOptions === null) { return; }
|
||||||
|
this._ankiConnect.enabled = value;
|
||||||
|
|
||||||
|
for (const cardController of this._selectorObserver.datas()) {
|
||||||
|
cardController.updateAnkiState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createCardController(node) {
|
||||||
|
const cardController = new AnkiCardController(this._settingsController, this, node);
|
||||||
|
cardController.prepare(this._ankiOptions);
|
||||||
|
return cardController;
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeCardController(node, cardController) {
|
||||||
|
cardController.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isCardControllerStale(node, cardController) {
|
||||||
|
return cardController.isStale();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getAnkiData() {
|
||||||
|
const [
|
||||||
|
[deckNames, error1],
|
||||||
|
[modelNames, error2]
|
||||||
|
] = await Promise.all([
|
||||||
|
this._getDeckNames(),
|
||||||
|
this._getModelNames()
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
_fieldsToDict(elements) {
|
if (error1 !== null) {
|
||||||
const result = {};
|
this._showAnkiError(error1);
|
||||||
for (const element of elements) {
|
} else if (error2 !== null) {
|
||||||
result[element.dataset.field] = element.value;
|
this._showAnkiError(error2);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
_spinnerShow(show) {
|
|
||||||
const spinner = document.querySelector('#anki-spinner');
|
|
||||||
spinner.hidden = !show;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setError(error) {
|
|
||||||
const node = document.querySelector('#anki-error');
|
|
||||||
const node2 = document.querySelector('#anki-invalid-response-error');
|
|
||||||
if (error) {
|
|
||||||
const errorString = `${error}`;
|
|
||||||
if (node !== null) {
|
|
||||||
node.hidden = false;
|
|
||||||
node.textContent = errorString;
|
|
||||||
this._setErrorData(node, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node2 !== null) {
|
|
||||||
node2.hidden = (errorString.indexOf('Invalid response') < 0);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (node !== null) {
|
this._hideAnkiError();
|
||||||
node.hidden = true;
|
}
|
||||||
node.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node2 !== null) {
|
return {deckNames, modelNames};
|
||||||
node2.hidden = true;
|
}
|
||||||
}
|
|
||||||
|
async _getDeckNames() {
|
||||||
|
try {
|
||||||
|
const result = await this._ankiConnect.getDeckNames();
|
||||||
|
return [result, null];
|
||||||
|
} catch (e) {
|
||||||
|
return [[], e];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setErrorData(node, error) {
|
async _getModelNames() {
|
||||||
|
try {
|
||||||
|
const result = await this._ankiConnect.getModelNames();
|
||||||
|
return [result, null];
|
||||||
|
} catch (e) {
|
||||||
|
return [[], e];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideAnkiError() {
|
||||||
|
this._ankiErrorContainer.hidden = true;
|
||||||
|
this._ankiErrorMessageDetailsContainer.hidden = true;
|
||||||
|
this._ankiErrorInvalidResponseInfo.hidden = true;
|
||||||
|
this._ankiErrorMessageContainer.textContent = '';
|
||||||
|
this._ankiErrorMessageDetailsContainer.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_showAnkiError(error) {
|
||||||
|
const errorString = `${error}`;
|
||||||
|
this._ankiErrorMessageContainer.textContent = errorString;
|
||||||
|
|
||||||
const data = error.data;
|
const data = error.data;
|
||||||
let message = '';
|
let details = '';
|
||||||
if (typeof data !== 'undefined') {
|
if (typeof data !== 'undefined') {
|
||||||
message += `${JSON.stringify(data, null, 4)}\n\n`;
|
details += `${JSON.stringify(data, null, 4)}\n\n`;
|
||||||
}
|
}
|
||||||
message += `${error.stack}`.trimRight();
|
details += `${error.stack}`.trimRight();
|
||||||
|
this._ankiErrorMessageDetailsContainer.textContent = details;
|
||||||
|
|
||||||
const button = document.createElement('a');
|
this._ankiErrorContainer.hidden = false;
|
||||||
button.className = 'error-data-show-button';
|
this._ankiErrorMessageDetailsContainer.hidden = true;
|
||||||
|
this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0);
|
||||||
const content = document.createElement('div');
|
|
||||||
content.className = 'error-data-container';
|
|
||||||
content.textContent = message;
|
|
||||||
content.hidden = true;
|
|
||||||
|
|
||||||
button.addEventListener('click', () => content.hidden = !content.hidden, false);
|
|
||||||
|
|
||||||
node.appendChild(button);
|
|
||||||
node.appendChild(content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setDropdownOptions(dropdown, optionValues) {
|
async _requestClipboardReadPermission() {
|
||||||
|
const permissions = ['clipboardRead'];
|
||||||
|
|
||||||
|
if (await new Promise((resolve) => chrome.permissions.contains({permissions}, resolve))) {
|
||||||
|
// Already has permission
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await new Promise((resolve) => chrome.permissions.request({permissions}, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFieldMarkers(fieldValue) {
|
||||||
|
const pattern = /\{([\w-]+)\}/g;
|
||||||
|
const markers = [];
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(fieldValue)) !== null) {
|
||||||
|
markers.push(match[1]);
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnkiCardController {
|
||||||
|
constructor(settingsController, ankiController, node) {
|
||||||
|
this._settingsController = settingsController;
|
||||||
|
this._ankiController = ankiController;
|
||||||
|
this._node = node;
|
||||||
|
this._cardType = node.dataset.ankiCardType;
|
||||||
|
this._eventListeners = new EventListenerCollection();
|
||||||
|
this._fieldEventListeners = new EventListenerCollection();
|
||||||
|
this._deck = null;
|
||||||
|
this._model = null;
|
||||||
|
this._fields = null;
|
||||||
|
this._modelChangingTo = null;
|
||||||
|
this._ankiCardDeckSelect = null;
|
||||||
|
this._ankiCardModelSelect = null;
|
||||||
|
this._ankiCardFieldsContainer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepare(ankiOptions) {
|
||||||
|
const cardOptions = this._getCardOptions(ankiOptions, this._cardType);
|
||||||
|
if (cardOptions === null) { return; }
|
||||||
|
const {deck, model, fields} = cardOptions;
|
||||||
|
this._deck = deck;
|
||||||
|
this._model = model;
|
||||||
|
this._fields = fields;
|
||||||
|
|
||||||
|
this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck');
|
||||||
|
this._ankiCardModelSelect = this._node.querySelector('.anki-card-model');
|
||||||
|
this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields');
|
||||||
|
|
||||||
|
this._setupSelects([], []);
|
||||||
|
this._setupFields();
|
||||||
|
|
||||||
|
this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false);
|
||||||
|
this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false);
|
||||||
|
|
||||||
|
await this.updateAnkiState();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this._eventListeners.removeAllEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAnkiState() {
|
||||||
|
if (this._fields === null) { return; }
|
||||||
|
const {deckNames, modelNames} = await this._ankiController.getAnkiData();
|
||||||
|
this._setupSelects(deckNames, modelNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
isStale() {
|
||||||
|
return (this._cardType !== this._node.dataset.ankiCardType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
_onCardDeckChange(e) {
|
||||||
|
this._setDeck(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCardModelChange(e) {
|
||||||
|
this._setModel(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFieldChange(e) {
|
||||||
|
this._ankiController.validateFieldPermissions(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFieldMarkerLinkClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const link = e.currentTarget;
|
||||||
|
const input = link.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value');
|
||||||
|
input.value = `{${link.textContent}}`;
|
||||||
|
input.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCardOptions(ankiOptions, cardType) {
|
||||||
|
switch (cardType) {
|
||||||
|
case 'terms': return ankiOptions.terms;
|
||||||
|
case 'kanji': return ankiOptions.kanji;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupSelects(deckNames, modelNames) {
|
||||||
|
const deck = this._deck;
|
||||||
|
const model = this._model;
|
||||||
|
if (!deckNames.includes(deck)) { deckNames = [...deckNames, deck]; }
|
||||||
|
if (!modelNames.includes(model)) { modelNames = [...modelNames, model]; }
|
||||||
|
|
||||||
|
this._setSelectOptions(this._ankiCardDeckSelect, deckNames);
|
||||||
|
this._ankiCardDeckSelect.value = deck;
|
||||||
|
|
||||||
|
this._setSelectOptions(this._ankiCardModelSelect, modelNames);
|
||||||
|
this._ankiCardModelSelect.value = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSelectOptions(select, optionValues) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const optionValue of optionValues) {
|
for (const optionValue of optionValues) {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
@ -184,128 +375,94 @@ class AnkiController {
|
|||||||
option.textContent = optionValue;
|
option.textContent = optionValue;
|
||||||
fragment.appendChild(option);
|
fragment.appendChild(option);
|
||||||
}
|
}
|
||||||
dropdown.textContent = '';
|
select.textContent = '';
|
||||||
dropdown.appendChild(fragment);
|
select.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _deckAndModelPopulate(options) {
|
_setupFields() {
|
||||||
const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
|
this._fieldEventListeners.removeAllEventListeners();
|
||||||
const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
|
|
||||||
const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
|
|
||||||
const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
|
|
||||||
try {
|
|
||||||
this._spinnerShow(true);
|
|
||||||
const [deckNames, modelNames] = await Promise.all([
|
|
||||||
this._ankiConnect.getDeckNames(),
|
|
||||||
this._ankiConnect.getModelNames()
|
|
||||||
]);
|
|
||||||
deckNames.sort();
|
|
||||||
modelNames.sort();
|
|
||||||
termsDeck.values = deckNames;
|
|
||||||
kanjiDeck.values = deckNames;
|
|
||||||
termsModel.values = modelNames;
|
|
||||||
kanjiModel.values = modelNames;
|
|
||||||
this._setError(null);
|
|
||||||
} catch (error) {
|
|
||||||
this._setError(error);
|
|
||||||
} finally {
|
|
||||||
this._spinnerShow(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
|
const markers = this._ankiController.getFieldMarkers(this._cardType);
|
||||||
const node = document.querySelector(selector);
|
const totalFragment = document.createDocumentFragment();
|
||||||
this._setDropdownOptions(node, Array.isArray(values) ? values : [value]);
|
for (const [fieldName, fieldValue] of Object.entries(this._fields)) {
|
||||||
node.value = value;
|
const content = this._settingsController.instantiateTemplateFragment('anki-card-field');
|
||||||
|
|
||||||
|
content.querySelector('.anki-card-field-name').textContent = fieldName;
|
||||||
|
|
||||||
|
const inputField = content.querySelector('.anki-card-field-value');
|
||||||
|
inputField.value = fieldValue;
|
||||||
|
inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]);
|
||||||
|
this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this), false);
|
||||||
|
|
||||||
|
const markerList = content.querySelector('.anki-card-field-marker-list');
|
||||||
|
if (markerList !== null) {
|
||||||
|
const markersFragment = this._ankiController.getFieldMarkersHtml(markers);
|
||||||
|
for (const element of markersFragment.querySelectorAll('.marker-link')) {
|
||||||
|
this._fieldEventListeners.addEventListener(element, 'click', this._onFieldMarkerLinkClick.bind(this), false);
|
||||||
|
}
|
||||||
|
markerList.appendChild(markersFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFragment.appendChild(content);
|
||||||
}
|
}
|
||||||
|
this._ankiCardFieldsContainer.textContent = '';
|
||||||
|
this._ankiCardFieldsContainer.appendChild(totalFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createFieldTemplate(name, value, markers) {
|
async _setDeck(value) {
|
||||||
const content = this._settingsController.instantiateTemplate('anki-field');
|
if (this._deck === value) { return; }
|
||||||
|
this._deck = value;
|
||||||
|
|
||||||
content.querySelector('.anki-field-name').textContent = name;
|
await this._settingsController.modifyProfileSettings([{
|
||||||
|
action: 'set',
|
||||||
const field = content.querySelector('.anki-field-value');
|
path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']),
|
||||||
field.dataset.field = name;
|
value
|
||||||
field.value = value;
|
}]);
|
||||||
|
|
||||||
content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers));
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _populateFields(tabId, fields) {
|
async _setModel(value) {
|
||||||
const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
|
if (this._modelChangingTo !== null) {
|
||||||
const container = tab.querySelector('tbody');
|
// Revert
|
||||||
const markers = this.getFieldMarkers(tabId);
|
this._ankiCardModelSelect.value = this._modelChangingTo;
|
||||||
|
return;
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
for (const [name, value] of Object.entries(fields)) {
|
|
||||||
const html = this._createFieldTemplate(name, value, markers);
|
|
||||||
fragment.appendChild(html);
|
|
||||||
}
|
}
|
||||||
|
if (this._model === value) { return; }
|
||||||
|
|
||||||
container.textContent = '';
|
|
||||||
container.appendChild(fragment);
|
|
||||||
|
|
||||||
for (const node of container.querySelectorAll('.anki-field-value')) {
|
|
||||||
node.addEventListener('change', this._onFieldsChanged.bind(this), false);
|
|
||||||
}
|
|
||||||
for (const node of container.querySelectorAll('.marker-link')) {
|
|
||||||
node.addEventListener('click', this._onMarkerClicked.bind(this), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMarkerClicked(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const link = e.currentTarget;
|
|
||||||
const input = link.closest('.input-group').querySelector('.anki-field-value');
|
|
||||||
input.value = `{${link.textContent}}`;
|
|
||||||
input.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onModelChanged(e) {
|
|
||||||
const node = e.currentTarget;
|
|
||||||
let fieldNames;
|
let fieldNames;
|
||||||
try {
|
try {
|
||||||
const modelName = node.value;
|
this._modelChangingTo = value;
|
||||||
fieldNames = await this._ankiConnect.getModelFieldNames(modelName);
|
fieldNames = await this._ankiController.getModelFieldNames(value);
|
||||||
this._setError(null);
|
} catch (e) {
|
||||||
} catch (error) {
|
// Revert
|
||||||
this._setError(error);
|
this._ankiCardModelSelect.value = this._model;
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
this._spinnerShow(false);
|
this._modelChangingTo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabId = node.dataset.ankiCardType;
|
|
||||||
if (tabId !== 'terms' && tabId !== 'kanji') { return; }
|
|
||||||
|
|
||||||
const fields = {};
|
const fields = {};
|
||||||
for (const name of fieldNames) {
|
for (const fieldName of fieldNames) {
|
||||||
fields[name] = '';
|
fields[fieldName] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._settingsController.setProfileSetting(`anki["${tabId}"].fields`, fields);
|
|
||||||
await this._populateFields(tabId, fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onFieldsChanged() {
|
|
||||||
const termsDeck = document.querySelector('#anki-terms-deck').value;
|
|
||||||
const termsModel = document.querySelector('#anki-terms-model').value;
|
|
||||||
const termsFields = this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value'));
|
|
||||||
const kanjiDeck = document.querySelector('#anki-kanji-deck').value;
|
|
||||||
const kanjiModel = document.querySelector('#anki-kanji-model').value;
|
|
||||||
const kanjiFields = this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value'));
|
|
||||||
|
|
||||||
const targets = [
|
const targets = [
|
||||||
{action: 'set', path: 'anki.terms.deck', value: termsDeck},
|
{
|
||||||
{action: 'set', path: 'anki.terms.model', value: termsModel},
|
action: 'set',
|
||||||
{action: 'set', path: 'anki.terms.fields', value: termsFields},
|
path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']),
|
||||||
{action: 'set', path: 'anki.kanji.deck', value: kanjiDeck},
|
value
|
||||||
{action: 'set', path: 'anki.kanji.model', value: kanjiModel},
|
},
|
||||||
{action: 'set', path: 'anki.kanji.fields', value: kanjiFields}
|
{
|
||||||
|
action: 'set',
|
||||||
|
path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']),
|
||||||
|
value: fields
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this._model = value;
|
||||||
|
this._fields = fields;
|
||||||
|
|
||||||
await this._settingsController.modifyProfileSettings(targets);
|
await this._settingsController.modifyProfileSettings(targets);
|
||||||
|
|
||||||
|
this._setupFields();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -882,7 +882,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" id="anki-enable" data-setting="anki.enable" data-transform-pre="setDocumentAttribute" data-transform-post="setDocumentAttribute" data-document-attribute="data-options-anki-enable"> Enable Anki integration</label>
|
<label><input type="checkbox" id="anki-enable" data-setting="anki.enable" data-transform="setDocumentAttribute" data-document-attribute="data-options-anki-enable"> Enable Anki integration</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="anki-general">
|
<div id="anki-general">
|
||||||
@ -894,9 +894,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-danger" id="anki-error" hidden></div>
|
<div class="alert alert-danger" id="anki-error" hidden>
|
||||||
|
<span id="anki-error-message"></span><a id="anki-error-message-details-toggle">…</a>
|
||||||
|
<div id="anki-error-message-details" hidden></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-danger" id="anki-invalid-response-error" hidden>
|
<div class="alert alert-danger" id="anki-error-invalid-response-info" hidden>
|
||||||
Attempting to connect to Anki can sometimes return an error message which includes "Invalid response",
|
Attempting to connect to Anki can sometimes return an error message which includes "Invalid response",
|
||||||
which may indicate that the value of the <strong>Interface server</strong> option is incorrect.
|
which may indicate that the value of the <strong>Interface server</strong> option is incorrect.
|
||||||
The <strong>Show advanced options</strong> checkbox under General Options must be ticked ticked to show this option.
|
The <strong>Show advanced options</strong> checkbox under General Options must be ticked ticked to show this option.
|
||||||
@ -955,41 +958,41 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content ignore-form-changes" id="anki-fields-container">
|
<div class="tab-content ignore-form-changes" id="anki-fields-container">
|
||||||
<div id="terms" class="tab-pane fade in active" data-anki-card-type="terms">
|
<div id="terms" class="tab-pane fade in active anki-card" data-anki-card-type="terms">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="anki-terms-deck">Deck</label>
|
<label for="anki-terms-deck">Deck</label>
|
||||||
<select class="form-control anki-deck" id="anki-terms-deck" data-anki-card-type="terms"></select>
|
<select class="form-control anki-card-deck" id="anki-terms-deck" data-anki-card-type="terms"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="anki-terms-model">Model</label>
|
<label for="anki-terms-model">Model</label>
|
||||||
<select class="form-control anki-model" id="anki-terms-model" data-anki-card-type="terms"></select>
|
<select class="form-control anki-card-model" id="anki-terms-model" data-anki-card-type="terms"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered anki-fields">
|
<table class="table table-bordered anki-fields">
|
||||||
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
||||||
<tbody></tbody>
|
<tbody class="anki-card-fields"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kanji" class="tab-pane fade" data-anki-card-type="kanji">
|
<div id="kanji" class="tab-pane fade anki-card" data-anki-card-type="kanji">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="anki-kanji-deck">Deck</label>
|
<label for="anki-kanji-deck">Deck</label>
|
||||||
<select class="form-control anki-deck" id="anki-kanji-deck" data-anki-card-type="kanji"></select>
|
<select class="form-control anki-card-deck" id="anki-kanji-deck" data-anki-card-type="kanji"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-xs-6">
|
<div class="form-group col-xs-6">
|
||||||
<label for="anki-kanji-model">Model</label>
|
<label for="anki-kanji-model">Model</label>
|
||||||
<select class="form-control anki-model" id="anki-kanji-model" data-anki-card-type="kanji"></select>
|
<select class="form-control anki-card-model" id="anki-kanji-model" data-anki-card-type="kanji"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered anki-fields">
|
<table class="table table-bordered anki-fields">
|
||||||
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
||||||
<tbody></tbody>
|
<tbody class="anki-card-fields"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1056,22 +1059,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template id="anki-field-template"><tr>
|
<template id="anki-card-field-template"><tr>
|
||||||
<td class="col-sm-2 anki-field-name"></td>
|
<td class="col-sm-2 anki-card-field-name"></td>
|
||||||
<td class="col-sm-10">
|
<td class="col-sm-10">
|
||||||
<div class="input-group">
|
<div class="input-group anki-card-field-value-container">
|
||||||
<input type="text" class="anki-field-value form-control" data-field="" value="">
|
<input type="text" class="anki-card-field-value form-control" data-field="" value="">
|
||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-right anki-field-marker-list"></ul>
|
<ul class="dropdown-menu dropdown-menu-right anki-card-field-marker-list"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr></template>
|
</tr></template>
|
||||||
|
|
||||||
<template id="anki-field-marker-template"><li><a class="marker-link" href="#"></a></li></template>
|
<template id="anki-card-field-marker-template"><li><a class="marker-link" href="#"></a></li></template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user