diff options
Diffstat (limited to 'ext/bg')
| -rw-r--r-- | ext/bg/css/settings.css | 7 | ||||
| -rw-r--r-- | ext/bg/js/settings/anki-controller.js | 471 | ||||
| -rw-r--r-- | ext/bg/settings.html | 37 | 
3 files changed, 336 insertions, 179 deletions
| diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 5d522209..231df1b3 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -331,17 +331,14 @@ input[type=checkbox].storage-button-checkbox {      display: none;  } -.error-data-show-button { +#anki-error-message-details-toggle {      display: inline-block;      margin-left: 0.5em;      cursor: pointer; -} -.error-data-show-button:after { -    content: "\2026";      font-weight: bold;  } -.error-data-container { +#anki-error-message-details {      margin-top: 0.25em;      font-family: 'Courier New', Courier, monospace;      white-space: pre; diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js index 373e4d43..e7ee2074 100644 --- a/ext/bg/js/settings/anki-controller.js +++ b/ext/bg/js/settings/anki-controller.js @@ -17,26 +17,47 @@  /* global   * AnkiConnect + * ObjectPropertyAccessor + * SelectorObserver   */  class AnkiController {      constructor(settingsController) { -        this._ankiConnect = new AnkiConnect();          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() { -        for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) { -            element.addEventListener('change', this._onFieldsChanged.bind(this), false); -        } +        this._ankiErrorContainer = document.querySelector('#anki-error'); +        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')) { -            element.addEventListener('change', this._onModelChanged.bind(this), false); -        } - -        this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); +        this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false); +        if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); }          const options = await this._settingsController.getOptions(); +        this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));          this._onOptionsChanged({options});      } @@ -92,220 +113,356 @@ class AnkiController {      getFieldMarkersHtml(markers) {          const fragment = document.createDocumentFragment();          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;              fragment.appendChild(markerNode);          }          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 -    async _onOptionsChanged({options}) { -        const {server, enable: enabled} = options.anki; -        this._ankiConnect.server = server; -        this._ankiConnect.enabled = enabled; +    async _onOptionsChanged({options: {anki}}) { +        this._ankiOptions = anki; +        this._ankiConnect.server = anki.server; +        this._ankiConnect.enabled = anki.enable; -        if (!enabled) { return; } +        this._selectorObserver.disconnect(); +        this._selectorObserver.observe(document.documentElement, true); +    } -        await this._deckAndModelPopulate(options); -        await Promise.all([ -            this._populateFields('terms', options.anki.terms.fields), -            this._populateFields('kanji', options.anki.kanji.fields) -        ]); +    _onAnkiErrorMessageDetailsToggleClick() { +        const node = this._ankiErrorMessageDetailsContainer; +        node.hidden = !node.hidden;      } -    _fieldsToDict(elements) { -        const result = {}; -        for (const element of elements) { -            result[element.dataset.field] = element.value; +    _onAnkiEnableChanged({detail: {value}}) { +        if (this._ankiOptions === null) { return; } +        this._ankiConnect.enabled = value; + +        for (const cardController of this._selectorObserver.datas()) { +            cardController.updateAnkiState();          } -        return result;      } -    _spinnerShow(show) { -        const spinner = document.querySelector('#anki-spinner'); -        spinner.hidden = !show; +    _createCardController(node) { +        const cardController = new AnkiCardController(this._settingsController, this, node); +        cardController.prepare(this._ankiOptions); +        return cardController;      } -    _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); -            } +    _removeCardController(node, cardController) { +        cardController.cleanup(); +    } -            if (node2 !== null) { -                node2.hidden = (errorString.indexOf('Invalid response') < 0); -            } +    _isCardControllerStale(node, cardController) { +        return cardController.isStale(); +    } + +    async _getAnkiData() { +        const [ +            [deckNames, error1], +            [modelNames, error2] +        ] = await Promise.all([ +            this._getDeckNames(), +            this._getModelNames() +        ]); + +        if (error1 !== null) { +            this._showAnkiError(error1); +        } else if (error2 !== null) { +            this._showAnkiError(error2);          } else { -            if (node !== null) { -                node.hidden = true; -                node.textContent = ''; -            } +            this._hideAnkiError(); +        } -            if (node2 !== null) { -                node2.hidden = true; -            } +        return {deckNames, modelNames}; +    } + +    async _getDeckNames() { +        try { +            const result = await this._ankiConnect.getDeckNames(); +            return [result, null]; +        } catch (e) { +            return [[], e];          }      } -    _setErrorData(node, error) { -        const data = error.data; -        let message = ''; -        if (typeof data !== 'undefined') { -            message += `${JSON.stringify(data, null, 4)}\n\n`; +    async _getModelNames() { +        try { +            const result = await this._ankiConnect.getModelNames(); +            return [result, null]; +        } catch (e) { +            return [[], e];          } -        message += `${error.stack}`.trimRight(); +    } -        const button = document.createElement('a'); -        button.className = 'error-data-show-button'; +    _hideAnkiError() { +        this._ankiErrorContainer.hidden = true; +        this._ankiErrorMessageDetailsContainer.hidden = true; +        this._ankiErrorInvalidResponseInfo.hidden = true; +        this._ankiErrorMessageContainer.textContent = ''; +        this._ankiErrorMessageDetailsContainer.textContent = ''; +    } -        const content = document.createElement('div'); -        content.className = 'error-data-container'; -        content.textContent = message; -        content.hidden = true; +    _showAnkiError(error) { +        const errorString = `${error}`; +        this._ankiErrorMessageContainer.textContent = errorString; -        button.addEventListener('click', () => content.hidden = !content.hidden, false); +        const data = error.data; +        let details = ''; +        if (typeof data !== 'undefined') { +            details += `${JSON.stringify(data, null, 4)}\n\n`; +        } +        details += `${error.stack}`.trimRight(); +        this._ankiErrorMessageDetailsContainer.textContent = details; -        node.appendChild(button); -        node.appendChild(content); +        this._ankiErrorContainer.hidden = false; +        this._ankiErrorMessageDetailsContainer.hidden = true; +        this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0);      } -    _setDropdownOptions(dropdown, optionValues) { -        const fragment = document.createDocumentFragment(); -        for (const optionValue of optionValues) { -            const option = document.createElement('option'); -            option.value = optionValue; -            option.textContent = optionValue; -            fragment.appendChild(option); +    async _requestClipboardReadPermission() { +        const permissions = ['clipboardRead']; + +        if (await new Promise((resolve) => chrome.permissions.contains({permissions}, resolve))) { +            // Already has permission +            return;          } -        dropdown.textContent = ''; -        dropdown.appendChild(fragment); + +        return await new Promise((resolve) => chrome.permissions.request({permissions}, resolve));      } -    async _deckAndModelPopulate(options) { -        const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'}; -        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); +    _getFieldMarkers(fieldValue) { +        const pattern = /\{([\w-]+)\}/g; +        const markers = []; +        let match; +        while ((match = pattern.exec(fieldValue)) !== null) { +            markers.push(match[1]);          } +        return markers; +    } +} -        for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) { -            const node = document.querySelector(selector); -            this._setDropdownOptions(node, Array.isArray(values) ? values : [value]); -            node.value = value; -        } +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;      } -    _createFieldTemplate(name, value, markers) { -        const content = this._settingsController.instantiateTemplate('anki-field'); +    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; -        content.querySelector('.anki-field-name').textContent = name; +        this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck'); +        this._ankiCardModelSelect = this._node.querySelector('.anki-card-model'); +        this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields'); -        const field = content.querySelector('.anki-field-value'); -        field.dataset.field = name; -        field.value = value; +        this._setupSelects([], []); +        this._setupFields(); -        content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers)); +        this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false); +        this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false); -        return content; +        await this.updateAnkiState();      } -    async _populateFields(tabId, fields) { -        const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`); -        const container = tab.querySelector('tbody'); -        const markers = this.getFieldMarkers(tabId); +    cleanup() { +        this._eventListeners.removeAllEventListeners(); +    } -        const fragment = document.createDocumentFragment(); -        for (const [name, value] of Object.entries(fields)) { -            const html = this._createFieldTemplate(name, value, markers); -            fragment.appendChild(html); -        } +    async updateAnkiState() { +        if (this._fields === null) { return; } +        const {deckNames, modelNames} = await this._ankiController.getAnkiData(); +        this._setupSelects(deckNames, modelNames); +    } -        container.textContent = ''; -        container.appendChild(fragment); +    isStale() { +        return (this._cardType !== this._node.dataset.ankiCardType); +    } -        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); -        } +    // Private + +    _onCardDeckChange(e) { +        this._setDeck(e.currentTarget.value); +    } + +    _onCardModelChange(e) { +        this._setModel(e.currentTarget.value); +    } + +    _onFieldChange(e) { +        this._ankiController.validateFieldPermissions(e.currentTarget.value);      } -    _onMarkerClicked(e) { +    _onFieldMarkerLinkClick(e) {          e.preventDefault();          const link = e.currentTarget; -        const input = link.closest('.input-group').querySelector('.anki-field-value'); +        const input = link.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value');          input.value = `{${link.textContent}}`;          input.dispatchEvent(new Event('change'));      } -    async _onModelChanged(e) { -        const node = e.currentTarget; +    _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(); +        for (const optionValue of optionValues) { +            const option = document.createElement('option'); +            option.value = optionValue; +            option.textContent = optionValue; +            fragment.appendChild(option); +        } +        select.textContent = ''; +        select.appendChild(fragment); +    } + +    _setupFields() { +        this._fieldEventListeners.removeAllEventListeners(); + +        const markers = this._ankiController.getFieldMarkers(this._cardType); +        const totalFragment = document.createDocumentFragment(); +        for (const [fieldName, fieldValue] of Object.entries(this._fields)) { +            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); +    } + +    async _setDeck(value) { +        if (this._deck === value) { return; } +        this._deck = value; + +        await this._settingsController.modifyProfileSettings([{ +            action: 'set', +            path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']), +            value +        }]); +    } + +    async _setModel(value) { +        if (this._modelChangingTo !== null) { +            // Revert +            this._ankiCardModelSelect.value = this._modelChangingTo; +            return; +        } +        if (this._model === value) { return; } +          let fieldNames;          try { -            const modelName = node.value; -            fieldNames = await this._ankiConnect.getModelFieldNames(modelName); -            this._setError(null); -        } catch (error) { -            this._setError(error); +            this._modelChangingTo = value; +            fieldNames = await this._ankiController.getModelFieldNames(value); +        } catch (e) { +            // Revert +            this._ankiCardModelSelect.value = this._model;              return;          } finally { -            this._spinnerShow(false); +            this._modelChangingTo = null;          } -        const tabId = node.dataset.ankiCardType; -        if (tabId !== 'terms' && tabId !== 'kanji') { return; } -          const fields = {}; -        for (const name of fieldNames) { -            fields[name] = ''; +        for (const fieldName of fieldNames) { +            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 = [ -            {action: 'set', path: 'anki.terms.deck',   value: termsDeck}, -            {action: 'set', path: 'anki.terms.model',  value: termsModel}, -            {action: 'set', path: 'anki.terms.fields', value: termsFields}, -            {action: 'set', path: 'anki.kanji.deck',   value: kanjiDeck}, -            {action: 'set', path: 'anki.kanji.model',  value: kanjiModel}, -            {action: 'set', path: 'anki.kanji.fields', value: kanjiFields} +            { +                action: 'set', +                path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']), +                value +            }, +            { +                action: 'set', +                path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']), +                value: fields +            }          ]; +        this._model = value; +        this._fields = fields; +          await this._settingsController.modifyProfileSettings(targets); + +        this._setupFields();      }  } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index e385d074..94eca586 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -882,7 +882,7 @@                  </p>                  <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 id="anki-general"> @@ -894,9 +894,12 @@                          </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",                          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. @@ -955,41 +958,41 @@                          </ul>                          <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="form-group col-xs-6">                                          <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 class="form-group col-xs-6">                                          <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>                                  <table class="table table-bordered anki-fields">                                      <thead><tr><th>Field</th><th>Value</th></tr></thead> -                                    <tbody></tbody> +                                    <tbody class="anki-card-fields"></tbody>                                  </table>                              </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="form-group col-xs-6">                                          <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 class="form-group col-xs-6">                                          <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>                                  <table class="table table-bordered anki-fields">                                      <thead><tr><th>Field</th><th>Value</th></tr></thead> -                                    <tbody></tbody> +                                    <tbody class="anki-card-fields"></tbody>                                  </table>                              </div>                          </div> @@ -1056,22 +1059,22 @@                              </div>                          </div> -                        <template id="anki-field-template"><tr> -                            <td class="col-sm-2 anki-field-name"></td> +                        <template id="anki-card-field-template"><tr> +                            <td class="col-sm-2 anki-card-field-name"></td>                              <td class="col-sm-10"> -                                <div class="input-group"> -                                    <input type="text" class="anki-field-value form-control" data-field="" value=""> +                                <div class="input-group anki-card-field-value-container"> +                                    <input type="text" class="anki-card-field-value form-control" data-field="" value="">                                      <div class="input-group-btn">                                          <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">                                              <span class="caret"></span>                                          </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>                              </td>                          </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> |