summaryrefslogtreecommitdiff
path: root/ext/js/display/display.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-13 22:52:28 -0500
committerGitHub <noreply@github.com>2021-02-13 22:52:28 -0500
commit6a271e067fa917614f4c81f473533e24c6d04404 (patch)
tree0d81658b1c03aecfbba133425aefc0ea7612338c /ext/js/display/display.js
parentdeed5027cd18bcdb9cb9d13cb7831be0ec5384e8 (diff)
Move mixed/js (#1383)
* Move mixed/js/core.js to js/core.js * Move mixed/js/yomichan.js to js/yomichan.js * Move mixed/js/timer.js to js/debug/timer.js * Move mixed/js/hotkey-handler.js to js/input/hotkey-handler.js * Move mixed/js/hotkey-help-controller.js to js/input/hotkey-help-controller.js * Move mixed/js/hotkey-util.js to js/input/hotkey-util.js * Move mixed/js/audio-system.js to js/input/audio-system.js * Move mixed/js/media-loader.js to js/input/media-loader.js * Move mixed/js/text-to-speech-audio.js to js/input/text-to-speech-audio.js * Move mixed/js/comm.js to js/comm/cross-frame-api.js * Move mixed/js/api.js to js/comm/api.js * Move mixed/js/frame-client.js to js/comm/frame-client.js * Move mixed/js/frame-endpoint.js to js/comm/frame-endpoint.js * Move mixed/js/display.js to js/display/display.js * Move mixed/js/display-audio.js to js/display/display-audio.js * Move mixed/js/display-generator.js to js/display/display-generator.js * Move mixed/js/display-history.js to js/display/display-history.js * Move mixed/js/display-notification.js to js/display/display-notification.js * Move mixed/js/display-profile-selection.js to js/display/display-profile-selection.js * Move mixed/js/japanese.js to js/language/japanese-util.js * Move mixed/js/dictionary-data-util.js to js/language/dictionary-data-util.js * Move mixed/js/document-focus-controller.js to js/dom/document-focus-controller.js * Move mixed/js/document-util.js to js/dom/document-util.js * Move mixed/js/dom-data-binder.js to js/dom/dom-data-binder.js * Move mixed/js/html-template-collection.js to js/dom/html-template-collection.js * Move mixed/js/panel-element.js to js/dom/panel-element.js * Move mixed/js/popup-menu.js to js/dom/popup-menu.js * Move mixed/js/selector-observer.js to js/dom/selector-observer.js * Move mixed/js/scroll.js to js/dom/window-scroll.js * Move mixed/js/text-scanner.js to js/language/text-scanner.js * Move mixed/js/cache-map.js to js/general/cache-map.js * Move mixed/js/object-property-accessor.js to js/general/object-property-accessor.js * Move mixed/js/task-accumulator.js to js/general/task-accumulator.js * Move mixed/js/environment.js to js/background/environment.js * Move mixed/js/dynamic-loader.js to js/scripting/dynamic-loader.js * Move mixed/js/dynamic-loader-sentinel.js to js/scripting/dynamic-loader-sentinel.js
Diffstat (limited to 'ext/js/display/display.js')
-rw-r--r--ext/js/display/display.js1886
1 files changed, 1886 insertions, 0 deletions
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
new file mode 100644
index 00000000..ffadd055
--- /dev/null
+++ b/ext/js/display/display.js
@@ -0,0 +1,1886 @@
+/*
+ * Copyright (C) 2017-2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * AnkiNoteBuilder
+ * DisplayAudio
+ * DisplayGenerator
+ * DisplayHistory
+ * DisplayNotification
+ * DocumentUtil
+ * FrameEndpoint
+ * Frontend
+ * HotkeyHelpController
+ * MediaLoader
+ * PopupFactory
+ * PopupMenu
+ * QueryParser
+ * TextScanner
+ * WindowScroll
+ * api
+ * dynamicLoader
+ */
+
+class Display extends EventDispatcher {
+ constructor(tabId, frameId, pageType, japaneseUtil, documentFocusController, hotkeyHandler) {
+ super();
+ this._tabId = tabId;
+ this._frameId = frameId;
+ this._pageType = pageType;
+ this._japaneseUtil = japaneseUtil;
+ this._documentFocusController = documentFocusController;
+ this._hotkeyHandler = hotkeyHandler;
+ this._container = document.querySelector('#definitions');
+ this._definitions = [];
+ this._definitionNodes = [];
+ this._optionsContext = {depth: 0, url: window.location.href};
+ this._options = null;
+ this._index = 0;
+ this._styleNode = null;
+ this._eventListeners = new EventListenerCollection();
+ this._setContentToken = null;
+ this._mediaLoader = new MediaLoader();
+ this._hotkeyHelpController = new HotkeyHelpController();
+ this._displayGenerator = new DisplayGenerator({
+ japaneseUtil,
+ mediaLoader: this._mediaLoader,
+ hotkeyHelpController: this._hotkeyHelpController
+ });
+ this._messageHandlers = new Map();
+ this._directMessageHandlers = new Map();
+ this._windowMessageHandlers = new Map();
+ this._history = new DisplayHistory({clearable: true, useBrowserHistory: false});
+ this._historyChangeIgnore = false;
+ this._historyHasChanged = false;
+ this._navigationHeader = document.querySelector('#navigation-header');
+ this._contentType = 'clear';
+ this._defaultTitle = document.title;
+ this._titleMaxLength = 1000;
+ this._query = '';
+ this._rawQuery = '';
+ this._fullQuery = '';
+ this._documentUtil = new DocumentUtil();
+ this._progressIndicator = document.querySelector('#progress-indicator');
+ this._progressIndicatorTimer = null;
+ this._progressIndicatorVisible = new DynamicProperty(false);
+ this._queryParserVisible = false;
+ this._queryParserVisibleOverride = null;
+ this._queryParserContainer = document.querySelector('#query-parser-container');
+ this._queryParser = new QueryParser({
+ getSearchContext: this._getSearchContext.bind(this),
+ documentUtil: this._documentUtil
+ });
+ this._ankiFieldTemplates = null;
+ this._ankiFieldTemplatesDefault = null;
+ this._ankiNoteBuilder = new AnkiNoteBuilder(true);
+ this._updateAdderButtonsPromise = Promise.resolve();
+ this._contentScrollElement = document.querySelector('#content-scroll');
+ this._contentScrollBodyElement = document.querySelector('#content-body');
+ this._windowScroll = new WindowScroll(this._contentScrollElement);
+ this._closeButton = document.querySelector('#close-button');
+ this._navigationPreviousButton = document.querySelector('#navigate-previous-button');
+ this._navigationNextButton = document.querySelector('#navigate-next-button');
+ this._frontend = null;
+ this._frontendSetupPromise = null;
+ this._depth = 0;
+ this._parentPopupId = null;
+ this._parentFrameId = null;
+ this._contentOriginTabId = tabId;
+ this._contentOriginFrameId = frameId;
+ this._childrenSupported = true;
+ this._frameEndpoint = (pageType === 'popup' ? new FrameEndpoint() : null);
+ this._browser = null;
+ this._copyTextarea = null;
+ this._definitionTextScanner = null;
+ this._frameResizeToken = null;
+ this._frameResizeHandle = document.querySelector('#frame-resizer-handle');
+ this._frameResizeStartSize = null;
+ this._frameResizeStartOffset = null;
+ this._frameResizeEventListeners = new EventListenerCollection();
+ this._tagNotification = null;
+ this._footerNotificationContainer = document.querySelector('#content-footer');
+ this._displayAudio = new DisplayAudio(this);
+ this._ankiNoteNotification = null;
+ this._ankiNoteNotificationEventListeners = null;
+ this._queryPostProcessor = null;
+
+ this._hotkeyHandler.registerActions([
+ ['close', () => { this._onHotkeyClose(); }],
+ ['nextEntry', () => { this._focusEntry(this._index + 1, true); }],
+ ['nextEntry3', () => { this._focusEntry(this._index + 3, true); }],
+ ['previousEntry', () => { this._focusEntry(this._index - 1, true); }],
+ ['previousEntry3', () => { this._focusEntry(this._index - 3, true); }],
+ ['lastEntry', () => { this._focusEntry(this._definitions.length - 1, true); }],
+ ['firstEntry', () => { this._focusEntry(0, true); }],
+ ['historyBackward', () => { this._sourceTermView(); }],
+ ['historyForward', () => { this._nextTermView(); }],
+ ['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedDefinition('kanji'); }],
+ ['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedDefinition('term-kanji'); }],
+ ['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedDefinition('term-kana'); }],
+ ['viewNote', () => { this._tryViewAnkiNoteForSelectedDefinition(); }],
+ ['playAudio', () => { this._playAudioCurrent(); }],
+ ['copyHostSelection', () => this._copyHostSelection()],
+ ['nextEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(1, true); }],
+ ['previousEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(-1, true); }]
+ ]);
+ this.registerDirectMessageHandlers([
+ ['setOptionsContext', {async: false, handler: this._onMessageSetOptionsContext.bind(this)}],
+ ['setContent', {async: false, handler: this._onMessageSetContent.bind(this)}],
+ ['clearAutoPlayTimer', {async: false, handler: this._onMessageClearAutoPlayTimer.bind(this)}],
+ ['setCustomCss', {async: false, handler: this._onMessageSetCustomCss.bind(this)}],
+ ['setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}],
+ ['configure', {async: true, handler: this._onMessageConfigure.bind(this)}]
+ ]);
+ this.registerWindowMessageHandlers([
+ ['extensionUnloaded', {async: false, handler: this._onMessageExtensionUnloaded.bind(this)}]
+ ]);
+ }
+
+ get displayGenerator() {
+ return this._displayGenerator;
+ }
+
+ get autoPlayAudioDelay() {
+ return this._displayAudio.autoPlayAudioDelay;
+ }
+
+ set autoPlayAudioDelay(value) {
+ this._displayAudio.autoPlayAudioDelay = value;
+ }
+
+ get queryParserVisible() {
+ return this._queryParserVisible;
+ }
+
+ set queryParserVisible(value) {
+ this._queryParserVisible = value;
+ this._updateQueryParser();
+ }
+
+ get japaneseUtil() {
+ return this._japaneseUtil;
+ }
+
+ get depth() {
+ return this._depth;
+ }
+
+ get hotkeyHandler() {
+ return this._hotkeyHandler;
+ }
+
+ get definitions() {
+ return this._definitions;
+ }
+
+ get definitionNodes() {
+ return this._definitionNodes;
+ }
+
+ get progressIndicatorVisible() {
+ return this._progressIndicatorVisible;
+ }
+
+ get tabId() {
+ return this._tabId;
+ }
+
+ get frameId() {
+ return this._frameId;
+ }
+
+ async prepare() {
+ // State setup
+ const {documentElement} = document;
+ const {browser} = await api.getEnvironmentInfo();
+ this._browser = browser;
+
+ // Prepare
+ await this._hotkeyHelpController.prepare();
+ await this._displayGenerator.prepare();
+ this._displayAudio.prepare();
+ this._queryParser.prepare();
+ this._history.prepare();
+
+ // Event setup
+ this._history.on('stateChanged', this._onStateChanged.bind(this));
+ this._queryParser.on('searched', this._onQueryParserSearch.bind(this));
+ this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this));
+ yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));
+ api.crossFrame.registerHandlers([
+ ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}]
+ ]);
+ window.addEventListener('message', this._onWindowMessage.bind(this), false);
+
+ if (this._pageType === 'popup' && documentElement !== null) {
+ documentElement.addEventListener('mouseup', this._onDocumentElementMouseUp.bind(this), false);
+ documentElement.addEventListener('click', this._onDocumentElementClick.bind(this), false);
+ documentElement.addEventListener('auxclick', this._onDocumentElementClick.bind(this), false);
+ }
+
+ document.addEventListener('wheel', this._onWheel.bind(this), {passive: false});
+ if (this._closeButton !== null) {
+ this._closeButton.addEventListener('click', this._onCloseButtonClick.bind(this), false);
+ }
+ if (this._navigationPreviousButton !== null) {
+ this._navigationPreviousButton.addEventListener('click', this._onSourceTermView.bind(this), false);
+ }
+ if (this._navigationNextButton !== null) {
+ this._navigationNextButton.addEventListener('click', this._onNextTermView.bind(this), false);
+ }
+
+ if (this._frameResizeHandle !== null) {
+ this._frameResizeHandle.addEventListener('mousedown', this._onFrameResizerMouseDown.bind(this), false);
+ }
+ }
+
+ getContentOrigin() {
+ return {
+ tabId: this._contentOriginTabId,
+ frameId: this._contentOriginFrameId
+ };
+ }
+
+ initializeState() {
+ this._onStateChanged();
+ if (this._frameEndpoint !== null) {
+ this._frameEndpoint.signal();
+ }
+ }
+
+ setHistorySettings({clearable, useBrowserHistory}) {
+ if (typeof clearable !== 'undefined') {
+ this._history.clearable = clearable;
+ }
+ if (typeof useBrowserHistory !== 'undefined') {
+ this._history.useBrowserHistory = useBrowserHistory;
+ }
+ }
+
+ onError(error) {
+ if (yomichan.isExtensionUnloaded) { return; }
+ yomichan.logError(error);
+ }
+
+ getOptions() {
+ return this._options;
+ }
+
+ getOptionsContext() {
+ return this._optionsContext;
+ }
+
+ async setOptionsContext(optionsContext) {
+ this._optionsContext = optionsContext;
+ await this.updateOptions();
+ }
+
+ async updateOptions() {
+ const options = await api.optionsGet(this.getOptionsContext());
+ const templates = await this._getAnkiFieldTemplates(options);
+ const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options;
+ this._options = options;
+ this._ankiFieldTemplates = templates;
+
+ this._updateHotkeys(options);
+ this._updateDocumentOptions(options);
+ this._updateTheme(options.general.popupTheme);
+ this.setCustomCss(options.general.customPopupCss);
+ this._displayAudio.updateOptions(options);
+ this._hotkeyHelpController.setOptions(options);
+ this._displayGenerator.updateHotkeys();
+ this._hotkeyHelpController.setupNode(document.documentElement);
+
+ this._queryParser.setOptions({
+ selectedParser: options.parsing.selectedParser,
+ termSpacing: options.parsing.termSpacing,
+ scanning: {
+ inputs: scanningOptions.inputs,
+ deepContentScan: scanningOptions.deepDomScan,
+ selectText: scanningOptions.selectText,
+ delay: scanningOptions.delay,
+ touchInputEnabled: scanningOptions.touchInputEnabled,
+ pointerEventsEnabled: scanningOptions.pointerEventsEnabled,
+ scanLength: scanningOptions.length,
+ layoutAwareScan: scanningOptions.layoutAwareScan,
+ preventMiddleMouse: scanningOptions.preventMiddleMouse.onSearchQuery,
+ sentenceParsingOptions
+ }
+ });
+
+ this._updateNestedFrontend(options);
+ this._updateDefinitionTextScanner(options);
+
+ this.trigger('optionsUpdated', {options});
+ }
+
+ clearAutoPlayTimer() {
+ this._displayAudio.clearAutoPlayTimer();
+ }
+
+ setContent(details) {
+ const {focus, history, params, state, content} = details;
+
+ if (focus) {
+ window.focus();
+ }
+
+ const urlSearchParams = new URLSearchParams();
+ for (const [key, value] of Object.entries(params)) {
+ urlSearchParams.append(key, value);
+ }
+ const url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`;
+
+ if (history && this._historyHasChanged) {
+ this._updateHistoryState();
+ this._history.pushState(state, content, url);
+ } else {
+ this._history.clear();
+ this._history.replaceState(state, content, url);
+ }
+ }
+
+ setCustomCss(css) {
+ if (this._styleNode === null) {
+ if (css.length === 0) { return; }
+ this._styleNode = document.createElement('style');
+ }
+
+ this._styleNode.textContent = css;
+
+ const parent = document.head;
+ if (this._styleNode.parentNode !== parent) {
+ parent.appendChild(this._styleNode);
+ }
+ }
+
+ registerDirectMessageHandlers(handlers) {
+ for (const [name, handlerInfo] of handlers) {
+ this._directMessageHandlers.set(name, handlerInfo);
+ }
+ }
+
+ registerWindowMessageHandlers(handlers) {
+ for (const [name, handlerInfo] of handlers) {
+ this._windowMessageHandlers.set(name, handlerInfo);
+ }
+ }
+
+ authenticateMessageData(data) {
+ if (this._frameEndpoint === null) {
+ return data;
+ }
+ if (!this._frameEndpoint.authenticate(data)) {
+ throw new Error('Invalid authentication');
+ }
+ return data.data;
+ }
+
+ setQueryPostProcessor(func) {
+ this._queryPostProcessor = func;
+ }
+
+ close() {
+ switch (this._pageType) {
+ case 'popup':
+ this._invokeContentOrigin('closePopup');
+ break;
+ case 'search':
+ this._closeTab();
+ break;
+ }
+ }
+
+ blurElement(element) {
+ this._documentFocusController.blurElement(element);
+ }
+
+ searchLast() {
+ const type = this._contentType;
+ if (type === 'clear') { return; }
+ const query = this._rawQuery;
+ const state = (
+ this._historyHasState() ?
+ clone(this._history.state) :
+ {
+ focusEntry: 0,
+ optionsContext: this._optionsContext,
+ url: window.location.href,
+ sentence: {text: query, offset: 0},
+ documentTitle: document.title
+ }
+ );
+ const details = {
+ focus: false,
+ history: false,
+ params: this._createSearchParams(type, query, false),
+ state,
+ content: {
+ definitions: null,
+ contentOrigin: this.getContentOrigin()
+ }
+ };
+ this.setContent(details);
+ }
+
+ // Message handlers
+
+ _onDirectMessage(data) {
+ data = this.authenticateMessageData(data);
+ const {action, params} = data;
+ const handlerInfo = this._directMessageHandlers.get(action);
+ if (typeof handlerInfo === 'undefined') {
+ throw new Error(`Invalid action: ${action}`);
+ }
+
+ const {async, handler} = handlerInfo;
+ const result = handler(params);
+ return {async, result};
+ }
+
+ _onWindowMessage({data}) {
+ try {
+ data = this.authenticateMessageData(data);
+ } catch (e) {
+ return;
+ }
+
+ const {action, params} = data;
+ const messageHandler = this._windowMessageHandlers.get(action);
+ if (typeof messageHandler === 'undefined') { return; }
+
+ const callback = () => {}; // NOP
+ yomichan.invokeMessageHandler(messageHandler, params, callback);
+ }
+
+ _onMessageSetOptionsContext({optionsContext}) {
+ this.setOptionsContext(optionsContext);
+ this.searchLast();
+ }
+
+ _onMessageSetContent({details}) {
+ this.setContent(details);
+ }
+
+ _onMessageClearAutoPlayTimer() {
+ this.clearAutoPlayTimer();
+ }
+
+ _onMessageSetCustomCss({css}) {
+ this.setCustomCss(css);
+ }
+
+ _onMessageSetContentScale({scale}) {
+ this._setContentScale(scale);
+ }
+
+ async _onMessageConfigure({depth, parentPopupId, parentFrameId, childrenSupported, scale, optionsContext}) {
+ this._depth = depth;
+ this._parentPopupId = parentPopupId;
+ this._parentFrameId = parentFrameId;
+ this._childrenSupported = childrenSupported;
+ this._setContentScale(scale);
+ await this.setOptionsContext(optionsContext);
+ }
+
+ _onMessageExtensionUnloaded() {
+ if (yomichan.isExtensionUnloaded) { return; }
+ yomichan.triggerExtensionUnloaded();
+ }
+
+ // Private
+
+ async _onStateChanged() {
+ if (this._historyChangeIgnore) { return; }
+
+ const token = {}; // Unique identifier token
+ this._setContentToken = token;
+ try {
+ // Clear
+ this._closePopups();
+ this._closeAllPopupMenus();
+ this._eventListeners.removeAllEventListeners();
+ this._mediaLoader.unloadAll();
+ this._displayAudio.cleanupEntries();
+ this._hideTagNotification(false);
+ this._hideAnkiNoteErrors(false);
+ this._definitions = [];
+ this._definitionNodes = [];
+
+ // Prepare
+ const urlSearchParams = new URLSearchParams(location.search);
+ let type = urlSearchParams.get('type');
+ if (type === null) { type = 'terms'; }
+
+ const fullVisible = urlSearchParams.get('full-visible');
+ this._queryParserVisibleOverride = (fullVisible === null ? null : (fullVisible !== 'false'));
+ this._updateQueryParser();
+
+ let clear = true;
+ this._historyHasChanged = true;
+ this._contentType = type;
+ this._query = '';
+ this._rawQuery = '';
+ const eventArgs = {type, urlSearchParams, token};
+
+ // Set content
+ switch (type) {
+ case 'terms':
+ case 'kanji':
+ {
+ let query = urlSearchParams.get('query');
+ if (!query) { break; }
+
+ this._query = query;
+ clear = false;
+ const isTerms = (type === 'terms');
+ query = this._postProcessQuery(query);
+ this._rawQuery = query;
+ let queryFull = urlSearchParams.get('full');
+ queryFull = (queryFull !== null ? this._postProcessQuery(queryFull) : query);
+ const wildcardsEnabled = (urlSearchParams.get('wildcards') !== 'off');
+ const lookup = (urlSearchParams.get('lookup') !== 'false');
+ await this._setContentTermsOrKanji(token, isTerms, query, queryFull, lookup, wildcardsEnabled, eventArgs);
+ }
+ break;
+ case 'unloaded':
+ {
+ clear = false;
+ const {content} = this._history;
+ eventArgs.content = content;
+ this.trigger('contentUpdating', eventArgs);
+ this._setContentExtensionUnloaded();
+ }
+ break;
+ }
+
+ // Clear
+ if (clear) {
+ type = 'clear';
+ this._contentType = type;
+ const {content} = this._history;
+ eventArgs.type = type;
+ eventArgs.content = content;
+ this.trigger('contentUpdating', eventArgs);
+ this._clearContent();
+ }
+
+ const stale = (this._setContentToken !== token);
+ eventArgs.stale = stale;
+ this.trigger('contentUpdated', eventArgs);
+ } catch (e) {
+ this.onError(e);
+ }
+ }
+
+ _onQueryParserSearch({type, definitions, sentence, inputInfo: {eventType}, textSource, optionsContext}) {
+ const query = textSource.text();
+ const historyState = this._history.state;
+ const history = (
+ eventType === 'click' ||
+ !isObject(historyState) ||
+ historyState.cause !== 'queryParser'
+ );
+ const details = {
+ focus: false,
+ history,
+ params: this._createSearchParams(type, query, false),
+ state: {
+ sentence,
+ optionsContext,
+ cause: 'queryParser'
+ },
+ content: {
+ definitions,
+ contentOrigin: this.getContentOrigin()
+ }
+ };
+ this.setContent(details);
+ }
+
+ _onExtensionUnloaded() {
+ const type = 'unloaded';
+ if (this._contentType === type) { return; }
+ const details = {
+ focus: false,
+ history: false,
+ params: {type},
+ state: {},
+ content: {
+ contentOrigin: {
+ tabId: this._tabId,
+ frameId: this._frameId
+ }
+ }
+ };
+ this.setContent(details);
+ }
+
+ _onCloseButtonClick(e) {
+ e.preventDefault();
+ this.close();
+ }
+
+ _onSourceTermView(e) {
+ e.preventDefault();
+ this._sourceTermView();
+ }
+
+ _onNextTermView(e) {
+ e.preventDefault();
+ this._nextTermView();
+ }
+
+ _onProgressIndicatorVisibleChanged({value}) {
+ if (this._progressIndicatorTimer !== null) {
+ clearTimeout(this._progressIndicatorTimer);
+ this._progressIndicatorTimer = null;
+ }
+
+ if (value) {
+ this._progressIndicator.hidden = false;
+ getComputedStyle(this._progressIndicator).getPropertyValue('display'); // Force update of CSS display property, allowing animation
+ this._progressIndicator.dataset.active = 'true';
+ } else {
+ this._progressIndicator.dataset.active = 'false';
+ this._progressIndicatorTimer = setTimeout(() => {
+ this._progressIndicator.hidden = true;
+ this._progressIndicatorTimer = null;
+ }, 250);
+ }
+ }
+
+ async _onKanjiLookup(e) {
+ try {
+ e.preventDefault();
+ if (!this._historyHasState()) { return; }
+
+ let {state: {sentence, url, documentTitle}} = this._history;
+ if (typeof url !== 'string') { url = window.location.href; }
+ if (typeof documentTitle !== 'string') { documentTitle = document.title; }
+ const optionsContext = this.getOptionsContext();
+ const query = e.currentTarget.textContent;
+ const definitions = await api.kanjiFind(query, optionsContext);
+ const details = {
+ focus: false,
+ history: true,
+ params: this._createSearchParams('kanji', query, false),
+ state: {
+ focusEntry: 0,
+ optionsContext,
+ url,
+ sentence,
+ documentTitle
+ },
+ content: {
+ definitions,
+ contentOrigin: this.getContentOrigin()
+ }
+ };
+ this.setContent(details);
+ } catch (error) {
+ this.onError(error);
+ }
+ }
+
+ _onNoteAdd(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ const index = this._getClosestDefinitionIndex(link);
+ this._addAnkiNote(index, link.dataset.mode);
+ }
+
+ _onNoteView(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ api.noteView(link.dataset.noteId);
+ }
+
+ _onWheel(e) {
+ if (e.altKey) {
+ if (e.deltaY !== 0) {
+ this._focusEntry(this._index + (e.deltaY > 0 ? 1 : -1), true);
+ e.preventDefault();
+ }
+ } else if (e.shiftKey) {
+ this._onHistoryWheel(e);
+ }
+ }
+
+ _onHistoryWheel(e) {
+ if (e.altKey) { return; }
+ const delta = -e.deltaX || e.deltaY;
+ if (delta > 0) {
+ this._sourceTermView();
+ e.preventDefault();
+ e.stopPropagation();
+ } else if (delta < 0) {
+ this._nextTermView();
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ _onDebugLogClick(e) {
+ const link = e.currentTarget;
+ const index = this._getClosestDefinitionIndex(link);
+ if (index < 0 || index >= this._definitions.length) { return; }
+ const definition = this._definitions[index];
+ console.log(definition);
+ }
+
+ _onDocumentElementMouseUp(e) {
+ switch (e.button) {
+ case 3: // Back
+ if (this._history.hasPrevious()) {
+ e.preventDefault();
+ }
+ break;
+ case 4: // Forward
+ if (this._history.hasNext()) {
+ e.preventDefault();
+ }
+ break;
+ }
+ }
+
+ _onDocumentElementClick(e) {
+ switch (e.button) {
+ case 3: // Back
+ if (this._history.hasPrevious()) {
+ e.preventDefault();
+ this._history.back();
+ }
+ break;
+ case 4: // Forward
+ if (this._history.hasNext()) {
+ e.preventDefault();
+ this._history.forward();
+ }
+ break;
+ }
+ }
+
+ _onEntryClick(e) {
+ if (e.button !== 0) { return; }
+ const node = e.currentTarget;
+ const index = parseInt(node.dataset.index, 10);
+ if (!Number.isFinite(index)) { return; }
+ this._entrySetCurrent(index);
+ }
+
+ _onTagClick(e) {
+ this._showTagNotification(e.currentTarget);
+ }
+
+ _showTagNotification(tagNode) {
+ if (this._tagNotification === null) {
+ const node = this._displayGenerator.createEmptyFooterNotification();
+ node.classList.add('click-scannable');
+ this._tagNotification = new DisplayNotification(this._footerNotificationContainer, node);
+ }
+
+ const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode);
+ this._tagNotification.setContent(content);
+ this._tagNotification.open();
+ }
+
+ _hideTagNotification(animate) {
+ if (this._tagNotification === null) { return; }
+ this._tagNotification.close(animate);
+ }
+
+ _updateDocumentOptions(options) {
+ const data = document.documentElement.dataset;
+ data.ankiEnabled = `${options.anki.enable}`;
+ data.glossaryLayoutMode = `${options.general.glossaryLayoutMode}`;
+ data.compactTags = `${options.general.compactTags}`;
+ data.enableSearchTags = `${options.scanning.enableSearchTags}`;
+ data.showPitchAccentDownstepNotation = `${options.general.showPitchAccentDownstepNotation}`;
+ data.showPitchAccentPositionNotation = `${options.general.showPitchAccentPositionNotation}`;
+ data.showPitchAccentGraph = `${options.general.showPitchAccentGraph}`;
+ data.debug = `${options.general.debugInfo}`;
+ data.popupDisplayMode = `${options.general.popupDisplayMode}`;
+ data.popupCurrentIndicatorMode = `${options.general.popupCurrentIndicatorMode}`;
+ data.popupActionBarVisibility = `${options.general.popupActionBarVisibility}`;
+ data.popupActionBarLocation = `${options.general.popupActionBarLocation}`;
+ }
+
+ _updateTheme(themeName) {
+ document.documentElement.dataset.theme = themeName;
+ }
+
+ async _findDefinitions(isTerms, source, wildcardsEnabled, optionsContext) {
+ if (isTerms) {
+ const findDetails = {};
+ if (wildcardsEnabled) {
+ const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(source);
+ if (match !== null) {
+ if (match[1]) {
+ findDetails.wildcard = 'prefix';
+ } else if (match[3]) {
+ findDetails.wildcard = 'suffix';
+ }
+ source = match[2];
+ }
+ }
+
+ const {definitions} = await api.termsFind(source, findDetails, optionsContext);
+ return definitions;
+ } else {
+ const definitions = await api.kanjiFind(source, optionsContext);
+ return definitions;
+ }
+ }
+
+ async _setContentTermsOrKanji(token, isTerms, query, queryFull, lookup, wildcardsEnabled, eventArgs) {
+ let {state, content} = this._history;
+ let changeHistory = false;
+ if (!isObject(content)) {
+ content = {};
+ changeHistory = true;
+ }
+ if (!isObject(state)) {
+ state = {};
+ changeHistory = true;
+ }
+
+ let {
+ focusEntry=null,
+ scrollX=null,
+ scrollY=null,
+ optionsContext=null
+ } = state;
+ if (typeof focusEntry !== 'number') { focusEntry = 0; }
+ if (!(typeof optionsContext === 'object' && optionsContext !== null)) {
+ optionsContext = this.getOptionsContext();
+ state.optionsContext = optionsContext;
+ changeHistory = true;
+ }
+
+ this._setFullQuery(queryFull);
+ this._setTitleText(query);
+
+ let {definitions} = content;
+ if (!Array.isArray(definitions)) {
+ definitions = lookup ? await this._findDefinitions(isTerms, query, wildcardsEnabled, optionsContext) : [];
+ if (this._setContentToken !== token) { return; }
+ content.definitions = definitions;
+ changeHistory = true;
+ }
+
+ let contentOriginValid = false;
+ const {contentOrigin} = content;
+ if (typeof contentOrigin === 'object' && contentOrigin !== null) {
+ const {tabId, frameId} = contentOrigin;
+ if (typeof tabId === 'number' && typeof frameId === 'number') {
+ this._contentOriginTabId = tabId;
+ this._contentOriginFrameId = frameId;
+ if (this._pageType === 'popup') {
+ this._hotkeyHandler.forwardFrameId = (tabId === this._tabId ? frameId : null);
+ }
+ contentOriginValid = true;
+ }
+ }
+ if (!contentOriginValid) {
+ content.contentOrigin = this.getContentOrigin();
+ changeHistory = true;
+ }
+
+ await this._setOptionsContextIfDifferent(optionsContext);
+ if (this._setContentToken !== token) { return; }
+
+ if (this._options === null) {
+ await this.updateOptions();
+ if (this._setContentToken !== token) { return; }
+ }
+
+ if (changeHistory) {
+ this._replaceHistoryStateNoNavigate(state, content);
+ }
+
+ eventArgs.source = query;
+ eventArgs.content = content;
+ this.trigger('contentUpdating', eventArgs);
+
+ this._definitions = definitions;
+
+ this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
+ this._setNoContentVisible(definitions.length === 0 && lookup);
+
+ const container = this._container;
+ container.textContent = '';
+
+ for (let i = 0, ii = definitions.length; i < ii; ++i) {
+ if (i > 0) {
+ await promiseTimeout(1);
+ if (this._setContentToken !== token) { return; }
+ }
+
+ const definition = definitions[i];
+ const entry = (
+ isTerms ?
+ this._displayGenerator.createTermEntry(definition) :
+ this._displayGenerator.createKanjiEntry(definition)
+ );
+ entry.dataset.index = `${i}`;
+ this._definitionNodes.push(entry);
+ this._addEntryEventListeners(entry);
+ this._displayAudio.setupEntry(entry, i);
+ container.appendChild(entry);
+ if (focusEntry === i) {
+ this._focusEntry(i, false);
+ }
+ }
+
+ if (typeof scrollX === 'number' || typeof scrollY === 'number') {
+ let {x, y} = this._windowScroll;
+ if (typeof scrollX === 'number') { x = scrollX; }
+ if (typeof scrollY === 'number') { y = scrollY; }
+ this._windowScroll.stop();
+ this._windowScroll.to(x, y);
+ }
+
+ this._displayAudio.setupEntriesComplete();
+
+ this._updateAdderButtons(token, isTerms, definitions);
+ }
+
+ _setContentExtensionUnloaded() {
+ const errorExtensionUnloaded = document.querySelector('#error-extension-unloaded');
+
+ if (this._container !== null) {
+ this._container.hidden = true;
+ }
+
+ if (errorExtensionUnloaded !== null) {
+ errorExtensionUnloaded.hidden = false;
+ }
+
+ this._updateNavigation(false, false);
+ this._setNoContentVisible(false);
+ this._setTitleText('');
+ this._setFullQuery('');
+ }
+
+ _clearContent() {
+ this._container.textContent = '';
+ this._setTitleText('');
+ this._setFullQuery('');
+ }
+
+ _setNoContentVisible(visible) {
+ const noResults = document.querySelector('#no-results');
+
+ if (noResults !== null) {
+ noResults.hidden = !visible;
+ }
+ }
+
+ _setFullQuery(text) {
+ this._fullQuery = text;
+ this._updateQueryParser();
+ }
+
+ _updateQueryParser() {
+ const text = this._fullQuery;
+ const visible = this._isQueryParserVisible();
+ this._queryParserContainer.hidden = !visible || text.length === 0;
+ if (visible && this._queryParser.text !== text) {
+ this._setQueryParserText(text);
+ }
+ }
+
+ async _setQueryParserText(text) {
+ const overrideToken = this._progressIndicatorVisible.setOverride(true);
+ try {
+ await this._queryParser.setText(text);
+ } finally {
+ this._progressIndicatorVisible.clearOverride(overrideToken);
+ }
+ }
+
+ _setTitleText(text) {
+ let title = this._defaultTitle;
+ if (text.length > 0) {
+ // Chrome limits title to 1024 characters
+ const ellipsis = '...';
+ const separator = ' - ';
+ const maxLength = this._titleMaxLength - title.length - separator.length;
+ if (text.length > maxLength) {
+ text = `${text.substring(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`;
+ }
+
+ title = `${text}${separator}${title}`;
+ }
+ document.title = title;
+ }
+
+ _updateNavigation(previous, next) {
+ const {documentElement} = document;
+ if (documentElement !== null) {
+ documentElement.dataset.hasNavigationPrevious = `${previous}`;
+ documentElement.dataset.hasNavigationNext = `${next}`;
+ }
+ if (this._navigationPreviousButton !== null) {
+ this._navigationPreviousButton.disabled = !previous;
+ }
+ if (this._navigationNextButton !== null) {
+ this._navigationNextButton.disabled = !next;
+ }
+ }
+
+ async _updateAdderButtons(token, isTerms, definitions) {
+ await this._updateAdderButtonsPromise;
+ if (this._setContentToken !== token) { return; }
+
+ const {promise, resolve} = deferPromise();
+ try {
+ this._updateAdderButtonsPromise = promise;
+
+ const modes = isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];
+ let states;
+ try {
+ if (this._options.anki.checkForDuplicates) {
+ const noteContext = this._getNoteContext();
+ states = await this._areDefinitionsAddable(definitions, modes, noteContext);
+ } else {
+ if (!await api.isAnkiConnected()) {
+ throw new Error('Anki not connected');
+ }
+ states = this._areDefinitionsAddableForcedValue(definitions, modes, true);
+ }
+ } catch (e) {
+ return;
+ }
+
+ if (this._setContentToken !== token) { return; }
+
+ this._updateAdderButtons2(states, modes);
+ } finally {
+ resolve();
+ }
+ }
+
+ _updateAdderButtons2(states, modes) {
+ for (let i = 0, ii = states.length; i < ii; ++i) {
+ const infos = states[i];
+ let noteId = null;
+ for (let j = 0, jj = infos.length; j < jj; ++j) {
+ const {canAdd, noteIds} = infos[j];
+ const mode = modes[j];
+ const button = this._adderButtonFind(i, mode);
+ if (button === null) {
+ continue;
+ }
+
+ if (Array.isArray(noteIds) && noteIds.length > 0) {
+ noteId = noteIds[0];
+ }
+ button.disabled = !canAdd;
+ button.hidden = false;
+ }
+ if (noteId !== null) {
+ this._viewerButtonShow(i, noteId);
+ }
+ }
+ }
+
+ _entrySetCurrent(index) {
+ const entryPre = this._getEntry(this._index);
+ if (entryPre !== null) {
+ entryPre.classList.remove('entry-current');
+ }
+
+ const entry = this._getEntry(index);
+ if (entry !== null) {
+ entry.classList.add('entry-current');
+ }
+
+ this._index = index;
+
+ return entry;
+ }
+
+ _focusEntry(index, smooth) {
+ index = Math.max(Math.min(index, this._definitions.length - 1), 0);
+
+ const entry = this._entrySetCurrent(index);
+ let target = index === 0 || entry === null ? 0 : this._getElementTop(entry);
+
+ if (this._navigationHeader !== null) {
+ target -= this._navigationHeader.getBoundingClientRect().height;
+ }
+
+ this._windowScroll.stop();
+ if (smooth) {
+ this._windowScroll.animate(this._windowScroll.x, target, 200);
+ } else {
+ this._windowScroll.toY(target);
+ }
+ }
+
+ _focusEntryWithDifferentDictionary(offset, smooth) {
+ const offsetSign = Math.sign(offset);
+ if (offsetSign === 0) { return false; }
+
+ let index = this._index;
+ const definitionCount = this._definitions.length;
+ if (index < 0 || index >= definitionCount) { return false; }
+
+ const {dictionary} = this._definitions[index];
+ for (let indexNext = index + offsetSign; indexNext >= 0 && indexNext < definitionCount; indexNext += offsetSign) {
+ const {dictionaryNames} = this._definitions[indexNext];
+ if (dictionaryNames.length > 1 || !dictionaryNames.includes(dictionary)) {
+ offset -= offsetSign;
+ if (Math.sign(offsetSign) !== offset) {
+ index = indexNext;
+ break;
+ }
+ }
+ }
+
+ if (index === this._index) { return false; }
+
+ this._focusEntry(index, smooth);
+ return true;
+ }
+
+ _sourceTermView() {
+ this._relativeTermView(false);
+ }
+
+ _nextTermView() {
+ this._relativeTermView(true);
+ }
+
+ _relativeTermView(next) {
+ if (next) {
+ return this._history.hasNext() && this._history.forward();
+ } else {
+ return this._history.hasPrevious() && this._history.back();
+ }
+ }
+
+ _tryAddAnkiNoteForSelectedDefinition(mode) {
+ this._addAnkiNote(this._index, mode);
+ }
+
+ _tryViewAnkiNoteForSelectedDefinition() {
+ const button = this._viewerButtonFind(this._index);
+ if (button !== null && !button.disabled) {
+ api.noteView(button.dataset.noteId);
+ }
+ }
+
+ async _addAnkiNote(definitionIndex, mode) {
+ if (definitionIndex < 0 || definitionIndex >= this._definitions.length) { return; }
+ const definition = this._definitions[definitionIndex];
+
+ const button = this._adderButtonFind(definitionIndex, mode);
+ if (button === null || button.disabled) { return; }
+
+ this._hideAnkiNoteErrors(true);
+
+ const errors = [];
+ const overrideToken = this._progressIndicatorVisible.setOverride(true);
+ try {
+ const {anki: {suspendNewCards}} = this._options;
+ const noteContext = this._getNoteContext();
+ const note = await this._createNote(definition, mode, noteContext, true, errors);
+
+ let noteId = null;
+ let addNoteOkay = false;
+ try {
+ noteId = await api.addAnkiNote(note);
+ addNoteOkay = true;
+ } catch (e) {
+ errors.length = 0;
+ errors.push(e);
+ }
+
+ if (addNoteOkay) {
+ if (noteId === null) {
+ errors.push(new Error('Note could not be added'));
+ } else {
+ if (suspendNewCards) {
+ try {
+ await api.suspendAnkiCardsForNote(noteId);
+ } catch (e) {
+ errors.push(e);
+ }
+ }
+ button.disabled = true;
+ this._viewerButtonShow(definitionIndex, noteId);
+ }
+ }
+ } catch (e) {
+ errors.push(e);
+ } finally {
+ this._progressIndicatorVisible.clearOverride(overrideToken);
+ }
+
+ if (errors.length > 0) {
+ this._showAnkiNoteErrors(errors);
+ } else {
+ this._hideAnkiNoteErrors(true);
+ }
+ }
+
+ _showAnkiNoteErrors(errors) {
+ if (this._ankiNoteNotificationEventListeners !== null) {
+ this._ankiNoteNotificationEventListeners.removeAllEventListeners();
+ }
+
+ if (this._ankiNoteNotification === null) {
+ const node = this._displayGenerator.createEmptyFooterNotification();
+ this._ankiNoteNotification = new DisplayNotification(this._footerNotificationContainer, node);
+ this._ankiNoteNotificationEventListeners = new EventListenerCollection();
+ }
+
+ const content = this._displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
+ for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
+ this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
+ console.log({ankiNoteErrors: errors});
+ }, false);
+ }
+
+ this._ankiNoteNotification.setContent(content);
+ this._ankiNoteNotification.open();
+ }
+
+ _hideAnkiNoteErrors(animate) {
+ if (this._ankiNoteNotification === null) { return; }
+ this._ankiNoteNotification.close(animate);
+ this._ankiNoteNotificationEventListeners.removeAllEventListeners();
+ }
+
+ async _playAudioCurrent() {
+ return await this._displayAudio.playAudio(this._index, 0);
+ }
+
+ _getEntry(index) {
+ const entries = this._definitionNodes;
+ return index >= 0 && index < entries.length ? entries[index] : null;
+ }
+
+ _getValidSentenceData(sentence) {
+ let {text, offset} = (isObject(sentence) ? sentence : {});
+ if (typeof text !== 'string') { text = ''; }
+ if (typeof offset !== 'number') { offset = 0; }
+ return {text, offset};
+ }
+
+ _getClosestDefinitionIndex(element) {
+ return this._getClosestIndex(element, '.entry');
+ }
+
+ _getClosestIndex(element, selector) {
+ const node = element.closest(selector);
+ if (node === null) { return -1; }
+ const index = parseInt(node.dataset.index, 10);
+ return Number.isFinite(index) ? index : -1;
+ }
+
+ _adderButtonFind(index, mode) {
+ const entry = this._getEntry(index);
+ return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
+ }
+
+ _viewerButtonFind(index) {
+ const entry = this._getEntry(index);
+ return entry !== null ? entry.querySelector('.action-view-note') : null;
+ }
+
+ _viewerButtonShow(index, noteId) {
+ const viewerButton = this._viewerButtonFind(index);
+ if (viewerButton === null) {
+ return;
+ }
+ viewerButton.disabled = false;
+ viewerButton.hidden = false;
+ viewerButton.dataset.noteId = noteId;
+ }
+
+ _getElementTop(element) {
+ const elementRect = element.getBoundingClientRect();
+ const documentRect = this._contentScrollBodyElement.getBoundingClientRect();
+ return elementRect.top - documentRect.top;
+ }
+
+ _getNoteContext() {
+ const {state} = this._history;
+ let {documentTitle, url, sentence} = (isObject(state) ? state : {});
+ if (typeof documentTitle !== 'string') {
+ documentTitle = '';
+ }
+ if (typeof url !== 'string') {
+ url = window.location.href;
+ }
+ sentence = this._getValidSentenceData(sentence);
+ return {
+ url,
+ sentence,
+ documentTitle
+ };
+ }
+
+ _historyHasState() {
+ return isObject(this._history.state);
+ }
+
+ _updateHistoryState() {
+ const {state, content} = this._history;
+ if (!isObject(state)) { return; }
+
+ state.focusEntry = this._index;
+ state.scrollX = this._windowScroll.x;
+ state.scrollY = this._windowScroll.y;
+ this._replaceHistoryStateNoNavigate(state, content);
+ }
+
+ _replaceHistoryStateNoNavigate(state, content) {
+ const historyChangeIgnorePre = this._historyChangeIgnore;
+ try {
+ this._historyChangeIgnore = true;
+ this._history.replaceState(state, content);
+ } finally {
+ this._historyChangeIgnore = historyChangeIgnorePre;
+ }
+ }
+
+ _createSearchParams(type, query, wildcards) {
+ const params = {};
+ if (query.length < this._fullQuery.length) {
+ params.full = this._fullQuery;
+ }
+ params.query = query;
+ if (typeof type === 'string') {
+ params.type = type;
+ }
+ if (!wildcards) {
+ params.wildcards = 'off';
+ }
+ if (this._queryParserVisibleOverride !== null) {
+ params['full-visible'] = `${this._queryParserVisibleOverride}`;
+ }
+ return params;
+ }
+
+ _isQueryParserVisible() {
+ return (
+ this._queryParserVisibleOverride !== null ?
+ this._queryParserVisibleOverride :
+ this._queryParserVisible
+ );
+ }
+
+ _closePopups() {
+ yomichan.trigger('closePopups');
+ }
+
+ async _getAnkiFieldTemplates(options) {
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates === 'string') { return templates; }
+
+ templates = this._ankiFieldTemplatesDefault;
+ if (typeof templates === 'string') { return templates; }
+
+ templates = await api.getDefaultAnkiFieldTemplates();
+ this._ankiFieldTemplatesDefault = templates;
+ return templates;
+ }
+
+ async _areDefinitionsAddable(definitions, modes, context) {
+ const modeCount = modes.length;
+ const notePromises = [];
+ for (const definition of definitions) {
+ for (const mode of modes) {
+ const notePromise = this._createNote(definition, mode, context, false, null);
+ notePromises.push(notePromise);
+ }
+ }
+ const notes = await Promise.all(notePromises);
+
+ const infos = await api.getAnkiNoteInfo(notes);
+ const results = [];
+ for (let i = 0, ii = infos.length; i < ii; i += modeCount) {
+ results.push(infos.slice(i, i + modeCount));
+ }
+ return results;
+ }
+
+ _areDefinitionsAddableForcedValue(definitions, modes, canAdd) {
+ const results = [];
+ const definitionCount = definitions.length;
+ const modeCount = modes.length;
+ for (let i = 0; i < definitionCount; ++i) {
+ const modeArray = [];
+ for (let j = 0; j < modeCount; ++j) {
+ modeArray.push({canAdd, noteIds: null});
+ }
+ results.push(modeArray);
+ }
+ return results;
+ }
+
+ async _createNote(definition, mode, context, injectMedia, errors) {
+ const options = this._options;
+ const templates = this._ankiFieldTemplates;
+ const {
+ general: {resultOutputMode, glossaryLayoutMode, compactTags},
+ anki: ankiOptions
+ } = options;
+ const {tags, checkForDuplicates, duplicateScope} = ankiOptions;
+ const modeOptions = (mode === 'kanji') ? ankiOptions.kanji : ankiOptions.terms;
+ const {deck: deckName, model: modelName} = modeOptions;
+ const fields = Object.entries(modeOptions.fields);
+
+ let injectedMedia = null;
+ if (injectMedia) {
+ let errors2;
+ ({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(definition, mode, options, fields));
+ if (Array.isArray(errors)) {
+ for (const error of errors2) {
+ errors.push(deserializeError(error));
+ }
+ }
+ }
+
+ return await this._ankiNoteBuilder.createNote({
+ definition,
+ mode,
+ context,
+ templates,
+ deckName,
+ modelName,
+ fields,
+ tags,
+ checkForDuplicates,
+ duplicateScope,
+ resultOutputMode,
+ glossaryLayoutMode,
+ compactTags,
+ injectedMedia,
+ errors
+ });
+ }
+
+ async _injectAnkiNoteMedia(definition, mode, options, fields) {
+ const {
+ anki: {screenshot: {format, quality}},
+ audio: {sources, customSourceUrl, customSourceType}
+ } = options;
+
+ const timestamp = Date.now();
+ const definitionDetails = this._getDefinitionDetailsForNote(definition);
+ const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl, customSourceType} : null);
+ const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} : null);
+ const clipboardDetails = {
+ image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'),
+ text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text')
+ };
+ return await api.injectAnkiNoteMedia(
+ timestamp,
+ definitionDetails,
+ audioDetails,
+ screenshotDetails,
+ clipboardDetails
+ );
+ }
+
+ _getDefinitionDetailsForNote(definition) {
+ const {type} = definition;
+ if (type === 'kanji') {
+ const {character} = definition;
+ return {type, character};
+ }
+
+ const termDetailsList = definition.expressions;
+ let bestIndex = -1;
+ for (let i = 0, ii = termDetailsList.length; i < ii; ++i) {
+ const {sourceTerm, expression, reading} = termDetailsList[i];
+ if (expression === sourceTerm) {
+ bestIndex = i;
+ break;
+ } else if (reading === sourceTerm && bestIndex < 0) {
+ bestIndex = i;
+ }
+ }
+ const {expression, reading} = termDetailsList[Math.max(0, bestIndex)];
+ return {type, expression, reading};
+ }
+
+ async _setOptionsContextIfDifferent(optionsContext) {
+ if (deepEqual(this._optionsContext, optionsContext)) { return; }
+ await this.setOptionsContext(optionsContext);
+ }
+
+ _setContentScale(scale) {
+ const body = document.body;
+ if (body === null) { return; }
+ body.style.fontSize = `${scale}em`;
+ }
+
+ async _updateNestedFrontend(options) {
+ const isSearchPage = (this._pageType === 'search');
+ const isEnabled = this._childrenSupported && (
+ (isSearchPage) ?
+ (options.scanning.enableOnSearchPage) :
+ (this._depth < options.scanning.popupNestingMaxDepth)
+ );
+
+ if (this._frontend === null) {
+ if (!isEnabled) { return; }
+
+ try {
+ if (this._frontendSetupPromise === null) {
+ this._frontendSetupPromise = this._setupNestedFrontend();
+ }
+ await this._frontendSetupPromise;
+ } catch (e) {
+ yomichan.logError(e);
+ return;
+ } finally {
+ this._frontendSetupPromise = null;
+ }
+ }
+
+ this._frontend.setDisabledOverride(!isEnabled);
+ }
+
+ async _setupNestedFrontend() {
+ const setupNestedPopupsOptions = {
+ useProxyPopup: this._parentFrameId !== null,
+ parentPopupId: this._parentPopupId,
+ parentFrameId: this._parentFrameId
+ };
+
+ await dynamicLoader.loadScripts([
+ '/js/language/text-scanner.js',
+ '/js/comm/frame-client.js',
+ '/fg/js/popup.js',
+ '/fg/js/popup-proxy.js',
+ '/fg/js/popup-window.js',
+ '/fg/js/popup-factory.js',
+ '/fg/js/frame-ancestry-handler.js',
+ '/fg/js/frame-offset-forwarder.js',
+ '/fg/js/frontend.js'
+ ]);
+
+ const popupFactory = new PopupFactory(this._frameId);
+ popupFactory.prepare();
+
+ Object.assign(setupNestedPopupsOptions, {
+ depth: this._depth + 1,
+ tabId: this._tabId,
+ frameId: this._frameId,
+ popupFactory,
+ pageType: this._pageType,
+ allowRootFramePopupProxy: true,
+ childrenSupported: this._childrenSupported,
+ hotkeyHandler: this._hotkeyHandler
+ });
+
+ const frontend = new Frontend(setupNestedPopupsOptions);
+ this._frontend = frontend;
+ await frontend.prepare();
+ }
+
+ async _invokeContentOrigin(action, params={}) {
+ if (this._contentOriginTabId === this._tabId && this._contentOriginFrameId === this._frameId) {
+ throw new Error('Content origin is same page');
+ }
+ return await api.crossFrame.invokeTab(this._contentOriginTabId, this._contentOriginFrameId, action, params);
+ }
+
+ _copyHostSelection() {
+ if (this._contentOriginFrameId === null || window.getSelection().toString()) { return false; }
+ this._copyHostSelectionInner();
+ return true;
+ }
+
+ async _copyHostSelectionInner() {
+ switch (this._browser) {
+ case 'firefox':
+ case 'firefox-mobile':
+ {
+ let text;
+ try {
+ text = await this._invokeContentOrigin('getSelectionText');
+ } catch (e) {
+ break;
+ }
+ this._copyText(text);
+ }
+ break;
+ default:
+ await this._invokeContentOrigin('copySelection');
+ break;
+ }
+ }
+
+ _copyText(text) {
+ const parent = document.body;
+ if (parent === null) { return; }
+
+ let textarea = this._copyTextarea;
+ if (textarea === null) {
+ textarea = document.createElement('textarea');
+ this._copyTextarea = textarea;
+ }
+
+ textarea.value = text;
+ parent.appendChild(textarea);
+ textarea.select();
+ document.execCommand('copy');
+ parent.removeChild(textarea);
+ }
+
+ _addMultipleEventListeners(container, selector, ...args) {
+ for (const node of container.querySelectorAll(selector)) {
+ this._eventListeners.addEventListener(node, ...args);
+ }
+ }
+
+ _addEntryEventListeners(entry) {
+ this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));
+ this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAdd.bind(this));
+ this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this));
+ this._addMultipleEventListeners(entry, '.kanji-link', 'click', this._onKanjiLookup.bind(this));
+ this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));
+ this._addMultipleEventListeners(entry, '.tag', 'click', this._onTagClick.bind(this));
+ }
+
+ _updateDefinitionTextScanner(options) {
+ if (!options.scanning.enablePopupSearch) {
+ if (this._definitionTextScanner !== null) {
+ this._definitionTextScanner.setEnabled(false);
+ }
+ return;
+ }
+
+ if (this._definitionTextScanner === null) {
+ this._definitionTextScanner = new TextScanner({
+ node: window,
+ getSearchContext: this._getSearchContext.bind(this),
+ documentUtil: this._documentUtil,
+ searchTerms: true,
+ searchKanji: false,
+ searchOnClick: true,
+ searchOnClickOnly: true
+ });
+ this._definitionTextScanner.includeSelector = '.click-scannable,.click-scannable *';
+ this._definitionTextScanner.excludeSelector = '.scan-disable,.scan-disable *';
+ this._definitionTextScanner.prepare();
+ this._definitionTextScanner.on('searched', this._onDefinitionTextScannerSearched.bind(this));
+ }
+
+ const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options;
+ this._definitionTextScanner.setOptions({
+ inputs: [{
+ include: 'mouse0',
+ exclude: '',
+ types: {mouse: true, pen: false, touch: false},
+ options: {
+ searchTerms: true,
+ searchKanji: true,
+ scanOnTouchMove: false,
+ scanOnPenHover: false,
+ scanOnPenPress: false,
+ scanOnPenRelease: false,
+ preventTouchScrolling: false
+ }
+ }],
+ deepContentScan: scanningOptions.deepDomScan,
+ selectText: false,
+ delay: scanningOptions.delay,
+ touchInputEnabled: false,
+ pointerEventsEnabled: false,
+ scanLength: scanningOptions.length,
+ layoutAwareScan: scanningOptions.layoutAwareScan,
+ preventMiddleMouse: false,
+ sentenceParsingOptions
+ });
+
+ this._definitionTextScanner.setEnabled(true);
+ }
+
+ _onDefinitionTextScannerSearched({type, definitions, sentence, textSource, optionsContext, error}) {
+ if (error !== null && !yomichan.isExtensionUnloaded) {
+ yomichan.logError(error);
+ }
+
+ if (type === null) { return; }
+
+ const query = textSource.text();
+ const url = window.location.href;
+ const documentTitle = document.title;
+ const details = {
+ focus: false,
+ history: true,
+ params: {
+ type,
+ query,
+ wildcards: 'off'
+ },
+ state: {
+ focusEntry: 0,
+ optionsContext,
+ url,
+ sentence,
+ documentTitle
+ },
+ content: {
+ definitions,
+ contentOrigin: this.getContentOrigin()
+ }
+ };
+ this._definitionTextScanner.clearSelection(true);
+ this.setContent(details);
+ }
+
+ _onFrameResizerMouseDown(e) {
+ if (e.button !== 0) { return; }
+ // Don't do e.preventDefault() here; this allows mousemove events to be processed
+ // if the pointer moves out of the frame.
+ this._startFrameResize(e);
+ }
+
+ _onFrameResizerMouseUp() {
+ this._stopFrameResize();
+ }
+
+ _onFrameResizerWindowBlur() {
+ this._stopFrameResize();
+ }
+
+ _onFrameResizerMouseMove(e) {
+ if ((e.buttons & 0x1) === 0x0) {
+ this._stopFrameResize();
+ } else {
+ if (this._frameResizeStartSize === null) { return; }
+ const {clientX: x, clientY: y} = e;
+ this._updateFrameSize(x, y);
+ }
+ }
+
+ _getSearchContext() {
+ return {optionsContext: this.getOptionsContext()};
+ }
+
+ _startFrameResize(e) {
+ if (this._frameResizeToken !== null) { return; }
+
+ const {clientX: x, clientY: y} = e;
+ const token = {};
+ this._frameResizeToken = token;
+ this._frameResizeStartOffset = {x, y};
+ this._frameResizeEventListeners.addEventListener(window, 'mouseup', this._onFrameResizerMouseUp.bind(this), false);
+ this._frameResizeEventListeners.addEventListener(window, 'blur', this._onFrameResizerWindowBlur.bind(this), false);
+ this._frameResizeEventListeners.addEventListener(window, 'mousemove', this._onFrameResizerMouseMove.bind(this), false);
+
+ const {documentElement} = document;
+ if (documentElement !== null) {
+ documentElement.dataset.isResizing = 'true';
+ }
+
+ this._initializeFrameResize(token);
+ }
+
+ async _initializeFrameResize(token) {
+ const size = await this._invokeContentOrigin('getFrameSize');
+ if (this._frameResizeToken !== token) { return; }
+ this._frameResizeStartSize = size;
+ }
+
+ _stopFrameResize() {
+ if (this._frameResizeToken === null) { return; }
+
+ this._frameResizeEventListeners.removeAllEventListeners();
+ this._frameResizeStartSize = null;
+ this._frameResizeStartOffset = null;
+ this._frameResizeToken = null;
+
+ const {documentElement} = document;
+ if (documentElement !== null) {
+ delete documentElement.dataset.isResizing;
+ }
+ }
+
+ async _updateFrameSize(x, y) {
+ const handleSize = this._frameResizeHandle.getBoundingClientRect();
+ let {width, height} = this._frameResizeStartSize;
+ width += x - this._frameResizeStartOffset.x;
+ height += y - this._frameResizeStartOffset.y;
+ width = Math.max(Math.max(0, handleSize.width), width);
+ height = Math.max(Math.max(0, handleSize.height), height);
+ await this._invokeContentOrigin('setFrameSize', {width, height});
+ }
+
+ _updateHotkeys(options) {
+ this._hotkeyHandler.setHotkeys(this._pageType, options.inputs.hotkeys);
+ }
+
+ async _closeTab() {
+ const tab = await new Promise((resolve, reject) => {
+ chrome.tabs.getCurrent((result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
+ }
+ });
+ });
+ const tabId = tab.id;
+ await new Promise((resolve, reject) => {
+ chrome.tabs.remove(tabId, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+
+ _onHotkeyClose() {
+ if (this._closeSinglePopupMenu()) { return; }
+ this.close();
+ }
+
+ _closeAllPopupMenus() {
+ for (const popupMenu of PopupMenu.openMenus) {
+ popupMenu.close();
+ }
+ }
+
+ _closeSinglePopupMenu() {
+ for (const popupMenu of PopupMenu.openMenus) {
+ popupMenu.close();
+ return true;
+ }
+ return false;
+ }
+
+ _postProcessQuery(query) {
+ const queryPostProcessor = this._queryPostProcessor;
+ return typeof queryPostProcessor === 'function' ? queryPostProcessor(query) : query;
+ }
+}