diff options
| author | Darius Jahandarie <djahandarie@gmail.com> | 2023-12-06 03:53:16 +0000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-06 03:53:16 +0000 | 
| commit | bd5bc1a5db29903bc098995cd9262c4576bf76af (patch) | |
| tree | c9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/display/display-anki.js | |
| parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
| parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) | |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/display/display-anki.js')
| -rw-r--r-- | ext/js/display/display-anki.js | 328 | 
1 files changed, 280 insertions, 48 deletions
| diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 2f94e414..d0da568c 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -16,54 +16,95 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ -import {EventListenerCollection, deferPromise, isObject} from '../core.js'; +import {EventListenerCollection, deferPromise} from '../core.js';  import {AnkiNoteBuilder} from '../data/anki-note-builder.js';  import {AnkiUtil} from '../data/anki-util.js';  import {PopupMenu} from '../dom/popup-menu.js';  import {yomitan} from '../yomitan.js';  export class DisplayAnki { +    /** +     * @param {import('./display.js').Display} display +     * @param {import('./display-audio.js').DisplayAudio} displayAudio +     * @param {import('../language/sandbox/japanese-util.js').JapaneseUtil} japaneseUtil +     */      constructor(display, displayAudio, japaneseUtil) { +        /** @type {import('./display.js').Display} */          this._display = display; +        /** @type {import('./display-audio.js').DisplayAudio} */          this._displayAudio = displayAudio; +        /** @type {?string} */          this._ankiFieldTemplates = null; +        /** @type {?string} */          this._ankiFieldTemplatesDefault = null; +        /** @type {AnkiNoteBuilder} */          this._ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); +        /** @type {?import('./display-notification.js').DisplayNotification} */          this._errorNotification = null; +        /** @type {?EventListenerCollection} */          this._errorNotificationEventListeners = null; +        /** @type {?import('./display-notification.js').DisplayNotification} */          this._tagsNotification = null; -        this._updateAdderButtonsPromise = Promise.resolve(); +        /** @type {?Promise<void>} */ +        this._updateAdderButtonsPromise = null; +        /** @type {?import('core').TokenObject} */          this._updateDictionaryEntryDetailsToken = null; +        /** @type {EventListenerCollection} */          this._eventListeners = new EventListenerCollection(); +        /** @type {?import('display-anki').DictionaryEntryDetails[]} */          this._dictionaryEntryDetails = null; +        /** @type {?import('anki-templates-internal').Context} */          this._noteContext = null; +        /** @type {boolean} */          this._checkForDuplicates = false; +        /** @type {boolean} */          this._suspendNewCards = false; +        /** @type {boolean} */          this._compactTags = false; +        /** @type {import('settings').ResultOutputMode} */          this._resultOutputMode = 'split'; +        /** @type {import('settings').GlossaryLayoutMode} */          this._glossaryLayoutMode = 'default'; +        /** @type {import('settings').AnkiDisplayTags} */          this._displayTags = 'never'; +        /** @type {import('settings').AnkiDuplicateScope} */          this._duplicateScope = 'collection'; +        /** @type {boolean} */          this._duplicateScopeCheckAllModels = false; +        /** @type {import('settings').AnkiScreenshotFormat} */          this._screenshotFormat = 'png'; +        /** @type {number} */          this._screenshotQuality = 100; +        /** @type {number} */          this._scanLength = 10; +        /** @type {import('settings').AnkiNoteGuiMode} */          this._noteGuiMode = 'browse'; +        /** @type {?number} */          this._audioDownloadIdleTimeout = null; +        /** @type {string[]} */          this._noteTags = []; +        /** @type {Map<import('display-anki').CreateMode, import('settings').AnkiNoteOptions>} */          this._modeOptions = new Map(); +        /** @type {Map<import('dictionary').DictionaryEntryType, import('display-anki').CreateMode[]>} */          this._dictionaryEntryTypeModeMap = new Map([              ['kanji', ['kanji']],              ['term', ['term-kanji', 'term-kana']]          ]); -        this._menuContainer = document.querySelector('#popup-menus'); +        /** @type {HTMLElement} */ +        this._menuContainer = /** @type {HTMLElement} */ (document.querySelector('#popup-menus')); +        /** @type {(event: MouseEvent) => void} */          this._onShowTagsBind = this._onShowTags.bind(this); +        /** @type {(event: MouseEvent) => void} */          this._onNoteAddBind = this._onNoteAdd.bind(this); +        /** @type {(event: MouseEvent) => void} */          this._onViewNoteButtonClickBind = this._onViewNoteButtonClick.bind(this); +        /** @type {(event: MouseEvent) => void} */          this._onViewNoteButtonContextMenuBind = this._onViewNoteButtonContextMenu.bind(this); +        /** @type {(event: import('popup-menu').MenuCloseEvent) => void} */          this._onViewNoteButtonMenuCloseBind = this._onViewNoteButtonMenuClose.bind(this);      } +    /** */      prepare() {          this._noteContext = this._getNoteContext();          this._display.hotkeyHandler.registerActions([ @@ -80,13 +121,16 @@ export class DisplayAnki {          this._display.on('logDictionaryEntryData', this._onLogDictionaryEntryData.bind(this));      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @returns {Promise<import('display-anki').LogData>} +     */      async getLogData(dictionaryEntry) { -        const result = {}; -          // Anki note data          let ankiNoteData;          let ankiNoteDataException;          try { +            if (this._noteContext === null) { throw new Error('Note context not initialized'); }              ankiNoteData = await this._ankiNoteBuilder.getRenderingData({                  dictionaryEntry,                  mode: 'test', @@ -99,12 +143,9 @@ export class DisplayAnki {          } catch (e) {              ankiNoteDataException = e;          } -        result.ankiNoteData = ankiNoteData; -        if (typeof ankiNoteDataException !== 'undefined') { -            result.ankiNoteDataException = ankiNoteDataException; -        }          // Anki notes +        /** @type {import('display-anki').AnkiNoteLogData[]} */          const ankiNotes = [];          const modes = this._getModes(dictionaryEntry.type === 'term');          for (const mode of modes) { @@ -114,8 +155,9 @@ export class DisplayAnki {              try {                  ({note: note, errors, requirements} = await this._createNote(dictionaryEntry, mode, []));              } catch (e) { -                errors = [e]; +                errors = [e instanceof Error ? e : new Error(`${e}`)];              } +            /** @type {import('display-anki').AnkiNoteLogData} */              const entry = {mode, note};              if (Array.isArray(errors) && errors.length > 0) {                  entry.errors = errors; @@ -125,13 +167,19 @@ export class DisplayAnki {              }              ankiNotes.push(entry);          } -        result.ankiNotes = ankiNotes; -        return result; +        return { +            ankiNoteData, +            ankiNoteDataException: ankiNoteDataException instanceof Error ? ankiNoteDataException : new Error(`${ankiNoteDataException}`), +            ankiNotes +        };      }      // Private +    /** +     * @param {import('display').OptionsUpdatedEvent} details +     */      _onOptionsUpdated({options}) {          const {              general: {resultOutputMode, glossaryLayoutMode, compactTags}, @@ -173,16 +221,21 @@ export class DisplayAnki {          this._updateAnkiFieldTemplates(options);      } +    /** */      _onContentClear() {          this._updateDictionaryEntryDetailsToken = null;          this._dictionaryEntryDetails = null;          this._hideErrorNotification(false);      } +    /** */      _onContentUpdateStart() {          this._noteContext = this._getNoteContext();      } +    /** +     * @param {import('display').ContentUpdateEntryEvent} details +     */      _onContentUpdateEntry({element}) {          const eventListeners = this._eventListeners;          for (const node of element.querySelectorAll('.action-button[data-action=view-tags]')) { @@ -198,45 +251,77 @@ export class DisplayAnki {          }      } +    /** */      _onContentUpdateComplete() {          this._updateDictionaryEntryDetails();      } +    /** +     * @param {import('display').LogDictionaryEntryDataEvent} details +     */      _onLogDictionaryEntryData({dictionaryEntry, promises}) {          promises.push(this.getLogData(dictionaryEntry));      } +    /** +     * @param {MouseEvent} e +     */      _onNoteAdd(e) {          e.preventDefault(); -        const node = e.currentTarget; -        const index = this._display.getElementDictionaryEntryIndex(node); -        this._addAnkiNote(index, node.dataset.mode); +        const element = /** @type {HTMLElement} */ (e.currentTarget); +        const mode = this._getValidCreateMode(element.dataset.mode); +        if (mode === null) { return; } +        const index = this._display.getElementDictionaryEntryIndex(element); +        this._addAnkiNote(index, mode);      } +    /** +     * @param {MouseEvent} e +     */      _onShowTags(e) {          e.preventDefault(); -        const tags = e.currentTarget.title; +        const element = /** @type {HTMLElement} */ (e.currentTarget); +        const tags = element.title;          this._showTagsNotification(tags);      } +    /** +     * @param {number} index +     * @param {import('display-anki').CreateMode} mode +     * @returns {?HTMLButtonElement} +     */      _adderButtonFind(index, mode) {          const entry = this._getEntry(index);          return entry !== null ? entry.querySelector(`.action-button[data-action=add-note][data-mode="${mode}"]`) : null;      } +    /** +     * @param {number} index +     * @returns {?HTMLButtonElement} +     */      _tagsIndicatorFind(index) {          const entry = this._getEntry(index);          return entry !== null ? entry.querySelector('.action-button[data-action=view-tags]') : null;      } +    /** +     * @param {number} index +     * @returns {?HTMLElement} +     */      _getEntry(index) {          const entries = this._display.dictionaryEntryNodes;          return index >= 0 && index < entries.length ? entries[index] : null;      } +    /** +     * @returns {?import('anki-templates-internal').Context} +     */      _getNoteContext() {          const {state} = this._display.history; -        let {documentTitle, url, sentence} = (isObject(state) ? state : {}); +        let documentTitle, url, sentence; +        if (typeof state === 'object' && state !== null) { +            ({documentTitle, url, sentence} = state); +        }          if (typeof documentTitle !== 'string') {              documentTitle = document.title;          } @@ -254,8 +339,10 @@ export class DisplayAnki {          };      } +    /** */      async _updateDictionaryEntryDetails() {          const {dictionaryEntries} = this._display; +        /** @type {?import('core').TokenObject} */          const token = {};          this._updateDictionaryEntryDetailsToken = token;          if (this._updateAdderButtonsPromise !== null) { @@ -263,13 +350,13 @@ export class DisplayAnki {          }          if (this._updateDictionaryEntryDetailsToken !== token) { return; } -        const {promise, resolve} = deferPromise(); +        const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails<void>} */ (deferPromise());          try {              this._updateAdderButtonsPromise = promise;              const dictionaryEntryDetails = await this._getDictionaryEntryDetails(dictionaryEntries);              if (this._updateDictionaryEntryDetailsToken !== token) { return; }              this._dictionaryEntryDetails = dictionaryEntryDetails; -            this._updateAdderButtons(); +            this._updateAdderButtons(dictionaryEntryDetails);          } finally {              resolve();              if (this._updateAdderButtonsPromise === promise) { @@ -278,9 +365,11 @@ export class DisplayAnki {          }      } -    _updateAdderButtons() { +    /** +     * @param {import('display-anki').DictionaryEntryDetails[]} dictionaryEntryDetails +     */ +    _updateAdderButtons(dictionaryEntryDetails) {          const displayTags = this._displayTags; -        const dictionaryEntryDetails = this._dictionaryEntryDetails;          for (let i = 0, ii = dictionaryEntryDetails.length; i < ii; ++i) {              let allNoteIds = null;              for (const {mode, canAdd, noteIds, noteInfos, ankiError} of dictionaryEntryDetails[i].modeMap.values()) { @@ -303,6 +392,10 @@ export class DisplayAnki {          }      } +    /** +     * @param {number} i +     * @param {(?import('anki').NoteInfo)[]} noteInfos +     */      _setupTagsIndicator(i, noteInfos) {          const tagsIndicator = this._tagsIndicatorFind(i);          if (tagsIndicator === null) { @@ -310,8 +403,9 @@ export class DisplayAnki {          }          const displayTags = new Set(); -        for (const {tags} of noteInfos) { -            for (const tag of tags) { +        for (const item of noteInfos) { +            if (item === null) { continue; } +            for (const tag of item.tags) {                  displayTags.add(tag);              }          } @@ -328,6 +422,9 @@ export class DisplayAnki {          }      } +    /** +     * @param {string} message +     */      _showTagsNotification(message) {          if (this._tagsNotification === null) {              this._tagsNotification = this._display.createNotification(true); @@ -337,11 +434,18 @@ export class DisplayAnki {          this._tagsNotification.open();      } +    /** +     * @param {import('display-anki').CreateMode} mode +     */      _tryAddAnkiNoteForSelectedEntry(mode) {          const index = this._display.selectedIndex;          this._addAnkiNote(index, mode);      } +    /** +     * @param {number} dictionaryEntryIndex +     * @param {import('display-anki').CreateMode} mode +     */      async _addAnkiNote(dictionaryEntryIndex, mode) {          const dictionaryEntries = this._display.dictionaryEntries;          const dictionaryEntryDetails = this._dictionaryEntryDetails; @@ -364,6 +468,7 @@ export class DisplayAnki {          this._hideErrorNotification(true); +        /** @type {Error[]} */          const allErrors = [];          const progressIndicatorVisible = this._display.progressIndicatorVisible;          const overrideToken = progressIndicatorVisible.setOverride(true); @@ -381,7 +486,7 @@ export class DisplayAnki {                  addNoteOkay = true;              } catch (e) {                  allErrors.length = 0; -                allErrors.push(e); +                allErrors.push(e instanceof Error ? e : new Error(`${e}`));              }              if (addNoteOkay) { @@ -392,7 +497,7 @@ export class DisplayAnki {                          try {                              await yomitan.api.suspendAnkiCardsForNote(noteId);                          } catch (e) { -                            allErrors.push(e); +                            allErrors.push(e instanceof Error ? e : new Error(`${e}`));                          }                      }                      button.disabled = true; @@ -400,7 +505,7 @@ export class DisplayAnki {                  }              }          } catch (e) { -            allErrors.push(e); +            allErrors.push(e instanceof Error ? e : new Error(`${e}`));          } finally {              progressIndicatorVisible.clearOverride(overrideToken);          } @@ -412,6 +517,11 @@ export class DisplayAnki {          }      } +    /** +     * @param {import('anki-note-builder').Requirement[]} requirements +     * @param {import('anki-note-builder').Requirement[]} outputRequirements +     * @returns {?DisplayAnkiError} +     */      _getAddNoteRequirementsError(requirements, outputRequirements) {          if (outputRequirements.length === 0) { return null; } @@ -429,12 +539,16 @@ export class DisplayAnki {          }          if (count === 0) { return null; } -        const error = new Error('The created card may not have some content'); +        const error = new DisplayAnkiError('The created card may not have some content');          error.requirements = requirements;          error.outputRequirements = outputRequirements;          return error;      } +    /** +     * @param {Error[]} errors +     * @param {(DocumentFragment|Node|Error)[]} [displayErrors] +     */      _showErrorNotification(errors, displayErrors) {          if (typeof displayErrors === 'undefined') { displayErrors = errors; } @@ -449,7 +563,7 @@ export class DisplayAnki {          const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(displayErrors);          for (const node of content.querySelectorAll('.anki-note-error-log-link')) { -            this._errorNotificationEventListeners.addEventListener(node, 'click', () => { +            /** @type {EventListenerCollection} */ (this._errorNotificationEventListeners).addEventListener(node, 'click', () => {                  console.log({ankiNoteErrors: errors});              }, false);          } @@ -458,16 +572,26 @@ export class DisplayAnki {          this._errorNotification.open();      } +    /** +     * @param {boolean} animate +     */      _hideErrorNotification(animate) {          if (this._errorNotification === null) { return; }          this._errorNotification.close(animate); -        this._errorNotificationEventListeners.removeAllEventListeners(); +        /** @type {EventListenerCollection} */ (this._errorNotificationEventListeners).removeAllEventListeners();      } +    /** +     * @param {import('settings').ProfileOptions} options +     */      async _updateAnkiFieldTemplates(options) {          this._ankiFieldTemplates = await this._getAnkiFieldTemplates(options);      } +    /** +     * @param {import('settings').ProfileOptions} options +     * @returns {Promise<string>} +     */      async _getAnkiFieldTemplates(options) {          let templates = options.anki.fieldTemplates;          if (typeof templates === 'string') { return templates; } @@ -480,6 +604,10 @@ export class DisplayAnki {          return templates;      } +    /** +     * @param {import('dictionary').DictionaryEntry[]} dictionaryEntries +     * @returns {Promise<import('display-anki').DictionaryEntryDetails[]>} +     */      async _getDictionaryEntryDetails(dictionaryEntries) {          const forceCanAddValue = (this._checkForDuplicates ? null : true);          const fetchAdditionalInfo = (this._displayTags !== 'never'); @@ -514,9 +642,10 @@ export class DisplayAnki {              }          } catch (e) {              infos = this._getAnkiNoteInfoForceValue(notes, false); -            ankiError = e; +            ankiError = e instanceof Error ? e : new Error(`${e}`);          } +        /** @type {import('display-anki').DictionaryEntryDetails[]} */          const results = [];          for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) {              results.push({ @@ -533,6 +662,11 @@ export class DisplayAnki {          return results;      } +    /** +     * @param {import('anki').Note[]} notes +     * @param {boolean} canAdd +     * @returns {import('anki').NoteInfoWrapper[]} +     */      _getAnkiNoteInfoForceValue(notes, canAdd) {          const results = [];          for (const note of notes) { @@ -542,11 +676,19 @@ export class DisplayAnki {          return results;      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @param {import('display-anki').CreateMode} mode +     * @param {import('anki-note-builder').Requirement[]} requirements +     * @returns {Promise<import('display-anki').CreateNoteResult>} +     */      async _createNote(dictionaryEntry, mode, requirements) {          const context = this._noteContext; +        if (context === null) { throw new Error('Note context not initialized'); }          const modeOptions = this._modeOptions.get(mode);          if (typeof modeOptions === 'undefined') { throw new Error(`Unsupported note type: ${mode}`); }          const template = this._ankiFieldTemplates; +        if (typeof template !== 'string') { throw new Error('Invalid template'); }          const {deck: deckName, model: modelName} = modeOptions;          const fields = Object.entries(modeOptions.fields);          const contentOrigin = this._display.getContentOrigin(); @@ -586,12 +728,26 @@ export class DisplayAnki {          return {note, errors, requirements: outputRequirements};      } +    /** +     * @param {boolean} isTerms +     * @returns {import('display-anki').CreateMode[]} +     */      _getModes(isTerms) {          return isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];      } +    /** +     * @param {unknown} sentence +     * @param {string} fallback +     * @param {number} fallbackOffset +     * @returns {import('anki-templates-internal').ContextSentence} +     */      _getValidSentenceData(sentence, fallback, fallbackOffset) { -        let {text, offset} = (isObject(sentence) ? sentence : {}); +        let text; +        let offset; +        if (typeof sentence === 'object' && sentence !== null) { +            ({text, offset} = /** @type {import('core').UnknownObject} */ (sentence)); +        }          if (typeof text !== 'string') {              text = fallback;              offset = fallbackOffset; @@ -601,6 +757,10 @@ export class DisplayAnki {          return {text, offset};      } +    /** +     * @param {import('api').InjectAnkiNoteMediaDefinitionDetails} details +     * @returns {?import('anki-note-builder').AudioMediaOptions} +     */      _getAnkiNoteMediaAudioDetails(details) {          if (details.type !== 'term') { return null; }          const {sources, preferredAudioIndex} = this._displayAudio.getAnkiNoteMediaAudioDetails(details.term, details.reading); @@ -609,56 +769,79 @@ export class DisplayAnki {      // View note functions +    /** +     * @param {MouseEvent} e +     */      _onViewNoteButtonClick(e) { +        const element = /** @type {HTMLElement} */ (e.currentTarget);          e.preventDefault();          if (e.shiftKey) { -            this._showViewNoteMenu(e.currentTarget); +            this._showViewNoteMenu(element);          } else { -            this._viewNote(e.currentTarget); +            this._viewNote(element);          }      } +    /** +     * @param {MouseEvent} e +     */      _onViewNoteButtonContextMenu(e) { +        const element = /** @type {HTMLElement} */ (e.currentTarget);          e.preventDefault(); -        this._showViewNoteMenu(e.currentTarget); +        this._showViewNoteMenu(element);      } +    /** +     * @param {import('popup-menu').MenuCloseEvent} e +     */      _onViewNoteButtonMenuClose(e) {          const {detail: {action, item}} = e;          switch (action) {              case 'viewNote': -                this._viewNote(item); +                if (item !== null) { +                    this._viewNote(item); +                }                  break;          }      } +    /** +     * @param {number} index +     * @param {number[]} noteIds +     * @param {boolean} prepend +     */      _updateViewNoteButton(index, noteIds, prepend) {          const button = this._getViewNoteButton(index);          if (button === null) { return; } +        /** @type {(number|string)[]} */ +        let allNoteIds = noteIds;          if (prepend) {              const currentNoteIds = button.dataset.noteIds;              if (typeof currentNoteIds === 'string' && currentNoteIds.length > 0) { -                noteIds = [...noteIds, currentNoteIds.split(' ')]; +                allNoteIds = [...allNoteIds, ...currentNoteIds.split(' ')];              }          } -        const disabled = (noteIds.length === 0); +        const disabled = (allNoteIds.length === 0);          button.disabled = disabled;          button.hidden = disabled; -        button.dataset.noteIds = noteIds.join(' '); +        button.dataset.noteIds = allNoteIds.join(' '); -        const badge = button.querySelector('.action-button-badge'); +        const badge = /** @type {?HTMLElement} */ (button.querySelector('.action-button-badge'));          if (badge !== null) {              const badgeData = badge.dataset; -            if (noteIds.length > 1) { +            if (allNoteIds.length > 1) {                  badgeData.icon = 'plus-thick'; -                badgeData.hidden = false; +                badge.hidden = false;              } else {                  delete badgeData.icon; -                badgeData.hidden = true; +                badge.hidden = true;              }          }      } +    /** +     * @param {HTMLElement} node +     */      async _viewNote(node) {          const noteIds = this._getNodeNoteIds(node);          if (noteIds.length === 0) { return; } @@ -666,26 +849,30 @@ export class DisplayAnki {              await yomitan.api.noteView(noteIds[0], this._noteGuiMode, false);          } catch (e) {              const displayErrors = ( -                e.message === 'Mode not supported' ? +                e instanceof Error && e.message === 'Mode not supported' ?                  [this._display.displayGenerator.instantiateTemplateFragment('footer-notification-anki-view-note-error')] :                  void 0              ); -            this._showErrorNotification([e], displayErrors); +            this._showErrorNotification([e instanceof Error ? e : new Error(`${e}`)], displayErrors);              return;          }      } +    /** +     * @param {HTMLElement} node +     */      _showViewNoteMenu(node) {          const noteIds = this._getNodeNoteIds(node);          if (noteIds.length === 0) { return; } -        const menuContainerNode = this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu'); -        const menuBodyNode = menuContainerNode.querySelector('.popup-menu-body'); +        const menuContainerNode = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu')); +        const menuBodyNode = /** @type {HTMLElement} */ (menuContainerNode.querySelector('.popup-menu-body'));          for (let i = 0, ii = noteIds.length; i < ii; ++i) {              const noteId = noteIds[i]; -            const item = this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu-item'); -            item.querySelector('.popup-menu-item-label').textContent = `Note ${i + 1}: ${noteId}`; +            const item = /** @type {HTMLElement} */ (this._display.displayGenerator.instantiateTemplate('view-note-button-popup-menu-item')); +            const label = /** @type {Element} */ (item.querySelector('.popup-menu-item-label')); +            label.textContent = `Note ${i + 1}: ${noteId}`;              item.dataset.menuAction = 'viewNote';              item.dataset.noteIds = `${noteId}`;              menuBodyNode.appendChild(item); @@ -696,6 +883,10 @@ export class DisplayAnki {          popupMenu.prepare();      } +    /** +     * @param {HTMLElement} node +     * @returns {number[]} +     */      _getNodeNoteIds(node) {          const {noteIds} = node.dataset;          const results = []; @@ -710,11 +901,16 @@ export class DisplayAnki {          return results;      } +    /** +     * @param {number} index +     * @returns {?HTMLButtonElement} +     */      _getViewNoteButton(index) {          const entry = this._getEntry(index);          return entry !== null ? entry.querySelector('.action-button[data-action=view-note]') : null;      } +    /** */      _viewNoteForSelectedEntry() {          const index = this._display.selectedIndex;          const button = this._getViewNoteButton(index); @@ -722,4 +918,40 @@ export class DisplayAnki {              this._viewNote(button);          }      } + +    /** +     * @param {string|undefined} value +     * @returns {?import('display-anki').CreateMode} +     */ +    _getValidCreateMode(value) { +        switch (value) { +            case 'kanji': +            case 'term-kanji': +            case 'term-kana': +                return value; +            default: +                return null; +        } +    } +} + +class DisplayAnkiError extends Error { +    /** +     * @param {string} message +     */ +    constructor(message) { +        super(message); +        /** @type {?import('anki-note-builder').Requirement[]} */ +        this._requirements = null; +        /** @type {?import('anki-note-builder').Requirement[]} */ +        this._outputRequirements = null; +    } + +    /** @type {?import('anki-note-builder').Requirement[]} */ +    get requirements() { return this._requirements; } +    set requirements(value) { this._requirements = value; } + +    /** @type {?import('anki-note-builder').Requirement[]} */ +    get outputRequirements() { return this._outputRequirements; } +    set outputRequirements(value) { this._outputRequirements = value; }  } |