diff options
-rw-r--r-- | .eslintrc.json | 1 | ||||
-rw-r--r-- | ext/bg/js/api.js | 12 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 12 | ||||
-rw-r--r-- | ext/bg/js/dictionary.js | 2 | ||||
-rw-r--r-- | ext/bg/js/handlebars.js | 5 | ||||
-rw-r--r-- | ext/bg/js/search.js | 6 | ||||
-rw-r--r-- | ext/bg/js/settings/dictionaries.js | 24 | ||||
-rw-r--r-- | ext/bg/js/settings/main.js | 4 | ||||
-rw-r--r-- | ext/bg/js/settings/popup-preview-frame.js | 2 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 30 | ||||
-rw-r--r-- | ext/fg/float.html | 2 | ||||
-rw-r--r-- | ext/fg/js/float.js | 38 | ||||
-rw-r--r-- | ext/fg/js/frontend-api-sender.js | 10 | ||||
-rw-r--r-- | ext/fg/js/frontend-initialize.js | 11 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 2 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy-host.js | 64 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy.js | 6 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 109 | ||||
-rw-r--r-- | ext/fg/js/source.js | 14 | ||||
-rw-r--r-- | ext/manifest.json | 1 | ||||
-rw-r--r-- | ext/mixed/js/api.js | 6 | ||||
-rw-r--r-- | ext/mixed/js/audio.js | 9 | ||||
-rw-r--r-- | ext/mixed/js/core.js | 45 | ||||
-rw-r--r-- | ext/mixed/js/display-generator.js | 26 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 131 | ||||
-rw-r--r-- | ext/mixed/js/text-scanner.js | 24 |
26 files changed, 318 insertions, 278 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index 39c5e964..a350bce4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -54,6 +54,7 @@ "stringReplaceAsync": "readonly", "parseUrl": "readonly", "EventDispatcher": "readonly", + "EventListenerCollection": "readonly", "EXTENSION_IS_BROWSER_EDGE": "readonly" } }, diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index cd6a9d18..0c244ffa 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -17,26 +17,18 @@ */ -function apiTemplateRender(template, data, dynamic) { - return _apiInvoke('templateRender', {data, template, dynamic}); +function apiTemplateRender(template, data) { + return _apiInvoke('templateRender', {data, template}); } function apiAudioGetUrl(definition, source, optionsContext) { return _apiInvoke('audioGetUrl', {definition, source, optionsContext}); } -function apiGetDisplayTemplatesHtml() { - return _apiInvoke('getDisplayTemplatesHtml'); -} - function apiClipboardGet() { return _apiInvoke('clipboardGet'); } -function apiGetQueryParserTemplatesHtml() { - return _apiInvoke('getQueryParserTemplatesHtml'); -} - function _apiInvoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 7b2ec46d..d1a34f82 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -18,7 +18,7 @@ /*global optionsSave, utilIsolate conditionsTestValue, profileConditionsDescriptor, profileOptionsGetDefaultFieldTemplates -handlebarsRenderDynamic, handlebarsRenderStatic +handlebarsRenderDynamic requestText, requestJson, optionsLoad dictConfigured, dictTermsSort, dictEnabledSet, dictNoteFormat audioGetUrl, audioInject @@ -88,7 +88,7 @@ class Backend { const callback = () => this.checkLastError(chrome.runtime.lastError); chrome.tabs.query({}, (tabs) => { for (const tab of tabs) { - chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback); + chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdated', params: {source}}, callback); } }); } @@ -459,12 +459,8 @@ class Backend { return this.anki.guiBrowse(`nid:${noteId}`); } - async _onApiTemplateRender({template, data, dynamic}) { - return ( - dynamic ? - handlebarsRenderDynamic(template, data) : - handlebarsRenderStatic(template, data) - ); + async _onApiTemplateRender({template, data}) { + return handlebarsRenderDynamic(template, data); } async _onApiCommandExec({command, params}) { diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index e03dece0..491632a0 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -335,7 +335,7 @@ async function dictFieldFormat(field, definition, mode, options, templates, exce } data.marker = marker; try { - return await apiTemplateRender(templates, data, true); + return await apiTemplateRender(templates, data); } catch (e) { if (exceptions) { exceptions.push(e); } return `{${marker}-render-error}`; diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js index e8cb67eb..b1443447 100644 --- a/ext/bg/js/handlebars.js +++ b/ext/bg/js/handlebars.js @@ -135,11 +135,6 @@ function handlebarsRegisterHelpers() { } } -function handlebarsRenderStatic(name, data) { - handlebarsRegisterHelpers(); - return Handlebars.templates[name](data).trim(); -} - function handlebarsRenderDynamic(template, data) { handlebarsRegisterHelpers(); const cache = handlebarsRenderDynamic._cache; diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 93bcfa53..76a62b97 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -49,9 +49,9 @@ class DisplaySearch extends Display { async prepare() { try { - await this.initialize(); - - await this.queryParser.prepare(); + const superPromise = super.prepare(); + const queryParserPromise = this.queryParser.prepare(); + await Promise.all([superPromise, queryParserPromise]); const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index fb459404..adad76fb 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -179,7 +179,7 @@ class SettingsDictionaryEntryUI { this.dictionaryInfo = dictionaryInfo; this.optionsDictionary = optionsDictionary; this.counts = null; - this.eventListeners = []; + this.eventListeners = new EventListenerCollection(); this.isDeleting = false; this.content = content; @@ -198,10 +198,10 @@ class SettingsDictionaryEntryUI { this.applyValues(); - this.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false); - this.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false); - this.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false); - this.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false); + this.eventListeners.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false); + this.eventListeners.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false); + this.eventListeners.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false); + this.eventListeners.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false); } cleanup() { @@ -212,7 +212,7 @@ class SettingsDictionaryEntryUI { this.content = null; } this.dictionaryInfo = null; - this.clearEventListeners(); + this.eventListeners.removeAllEventListeners(); } setCounts(counts) { @@ -229,18 +229,6 @@ class SettingsDictionaryEntryUI { this.parent.save(); } - addEventListener(node, type, listener, options) { - node.addEventListener(type, listener, options); - this.eventListeners.push([node, type, listener, options]); - } - - clearEventListeners() { - for (const [node, type, listener, options] of this.eventListeners) { - node.removeEventListener(type, listener, options); - } - this.eventListeners = []; - } - applyValues() { this.enabledCheckbox.checked = this.optionsDictionary.enabled; this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches; diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 1ba4a7ef..c6683427 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -242,7 +242,7 @@ async function settingsSaveOptions() { await apiOptionsSave(source); } -async function onOptionsUpdate({source}) { +async function onOptionsUpdated({source}) { const thisSource = await settingsGetSource(); if (source === thisSource) { return; } @@ -274,7 +274,7 @@ async function onReady() { storageInfoInitialize(); - yomichan.on('optionsUpdate', onOptionsUpdate); + yomichan.on('optionsUpdated', onOptionsUpdated); } $(document).ready(() => onReady()); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 042f335f..8fd06222 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -50,7 +50,7 @@ class SettingsPopupPreview { const popupHost = new PopupProxyHost(); await popupHost.prepare(); - const popup = popupHost.createPopup(null, 0); + const popup = popupHost.getOrCreatePopup(); popup.setChildrenSupported(false); this.frontend = new Frontend(popup); diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 81c2464b..3471cb01 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -27,7 +27,7 @@ class Translator { constructor() { this.database = null; this.deinflector = null; - this.tagCache = {}; + this.tagCache = new Map(); } async prepare() { @@ -44,12 +44,12 @@ class Translator { } async purgeDatabase() { - this.tagCache = {}; + this.tagCache.clear(); await this.database.purge(); } async deleteDictionary(dictionaryName) { - this.tagCache = {}; + this.tagCache.clear(); await this.database.deleteDictionary(dictionaryName); } @@ -460,7 +460,7 @@ class Translator { termList = []; expressionsUnique.push(expression); termsUnique.push(termList); - termsUniqueMap[expression] = termList; + termsUniqueMap.set(expression, termList); } termList.push(term); @@ -537,22 +537,22 @@ class Translator { async getTagMetaList(names, title) { const tagMetaList = []; - const cache = ( - hasOwn(this.tagCache, title) ? - this.tagCache[title] : - (this.tagCache[title] = {}) - ); + let cache = this.tagCache.get(title); + if (typeof cache === 'undefined') { + cache = new Map(); + this.tagCache.set(title, cache); + } for (const name of names) { const base = Translator.getNameBase(name); - if (hasOwn(cache, base)) { - tagMetaList.push(cache[base]); - } else { - const tagMeta = await this.database.findTagForTitle(base, title); - cache[base] = tagMeta; - tagMetaList.push(tagMeta); + let tagMeta = cache.get(base); + if (typeof tagMeta === 'undefined') { + tagMeta = await this.database.findTagForTitle(base, title); + cache.set(base, tagMeta); } + + tagMetaList.push(tagMeta); } return tagMetaList; diff --git a/ext/fg/float.html b/ext/fg/float.html index bec5ae68..082755f5 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -35,7 +35,7 @@ <h1>Yomichan Updated!</h1> <p> The Yomichan extension has been updated to a new version! In order to continue - viewing definitions on this page you must reload this tab or restart your browser. + viewing definitions on this page, you must reload this tab or restart your browser. </p> </div> </div> diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index d31b8336..440a9731 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -/*global popupNestedInitialize, Display*/ +/*global popupNestedInitialize, apiForward, Display*/ class DisplayFloat extends Display { constructor() { @@ -29,11 +29,31 @@ class DisplayFloat extends Display { }; this._orphaned = false; + this._prepareInvoked = false; yomichan.on('orphaned', () => this.onOrphaned()); window.addEventListener('message', (e) => this.onMessage(e), false); } + async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) { + if (this._prepareInvoked) { return; } + this._prepareInvoked = true; + + await super.prepare(options); + + const {id, depth, parentFrameId} = popupInfo; + this.optionsContext.depth = depth; + this.optionsContext.url = url; + + if (childrenSupported) { + popupNestedInitialize(id, depth, parentFrameId, url); + } + + this.setContentScale(scale); + + apiForward('popupPrepareCompleted', {uniqueId}); + } + onError(error) { if (this._orphaned) { this.setContent('orphaned'); @@ -93,20 +113,6 @@ class DisplayFloat extends Display { setContentScale(scale) { document.body.style.fontSize = `${scale}em`; } - - async initialize(options, popupInfo, url, childrenSupported, scale) { - await super.initialize(options); - - const {id, depth, parentFrameId} = popupInfo; - this.optionsContext.depth = depth; - this.optionsContext.url = url; - - if (childrenSupported) { - popupNestedInitialize(id, depth, parentFrameId, url); - } - - this.setContentScale(scale); - } } DisplayFloat._onKeyDownHandlers = new Map([ @@ -123,7 +129,7 @@ DisplayFloat._messageHandlers = new Map([ ['setContent', (self, {type, details}) => self.setContent(type, details)], ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()], ['setCustomCss', (self, {css}) => self.setCustomCss(css)], - ['initialize', (self, {options, popupInfo, url, childrenSupported, scale}) => self.initialize(options, popupInfo, url, childrenSupported, scale)], + ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)], ['setContentScale', (self, {scale}) => self.setContentScale(scale)] ]); diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index 93c2e593..8dc6aaf3 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -19,7 +19,7 @@ class FrontendApiSender { constructor() { - this.senderId = FrontendApiSender.generateId(16); + this.senderId = yomichan.generateId(16); this.ackTimeout = 3000; // 3 seconds this.responseTimeout = 10000; // 10 seconds this.callbacks = new Map(); @@ -123,12 +123,4 @@ class FrontendApiSender { info.timer = null; info.reject(new Error(reason)); } - - static generateId(length) { - let id = ''; - for (let i = 0; i < length; ++i) { - id += Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); - } - return id; - } } diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index c32e97d4..54b874f2 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -22,13 +22,16 @@ async function main() { const data = window.frontendInitializationData || {}; const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; - let popupHost = null; - if (!proxy) { - popupHost = new PopupProxyHost(); + let popup; + if (proxy) { + popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); + } else { + const popupHost = new PopupProxyHost(); await popupHost.prepare(); + + popup = popupHost.getOrCreatePopup(); } - const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : popupHost.createPopup(null, depth); const frontend = new Frontend(popup, ignoreNodes); await frontend.prepare(); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 3611d44e..67045241 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -56,7 +56,7 @@ class Frontend extends TextScanner { } yomichan.on('orphaned', () => this.onOrphaned()); - yomichan.on('optionsUpdate', () => this.updateOptions()); + yomichan.on('optionsUpdated', () => this.updateOptions()); yomichan.on('zoomChanged', (e) => this.onZoomChanged(e)); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 98729796..e55801ff 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -34,7 +34,7 @@ class PopupProxyHost { if (typeof frameId !== 'number') { return; } this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([ - ['createNestedPopup', ({parentId}) => this._onApiCreateNestedPopup(parentId)], + ['getOrCreatePopup', ({id, parentId}) => this._onApiGetOrCreatePopup(id, parentId)], ['setOptions', ({id, options}) => this._onApiSetOptions(id, options)], ['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)], ['isVisible', ({id}) => this._onApiIsVisibleAsync(id)], @@ -47,14 +47,51 @@ class PopupProxyHost { ])); } - createPopup(parentId, depth) { - return this._createPopupInternal(parentId, depth).popup; + getOrCreatePopup(id=null, parentId=null) { + // Find by existing id + if (id !== null) { + const popup = this._popups.get(id); + if (typeof popup !== 'undefined') { + return popup; + } + } + + // Find by existing parent id + let parent = null; + if (parentId !== null) { + parent = this._popups.get(parentId); + if (typeof parent !== 'undefined') { + const popup = parent.child; + if (popup !== null) { + return popup; + } + } else { + parent = null; + } + } + + // New unique id + if (id === null) { + id = this._nextId++; + } + + // Create new popup + const depth = (parent !== null ? parent.depth + 1 : 0); + const popup = new Popup(id, depth, this._frameIdPromise); + if (parent !== null) { + popup.setParent(parent); + } + this._popups.set(id, popup); + return popup; } // Message handlers - async _onApiCreateNestedPopup(parentId) { - return this._createPopupInternal(parentId, 0).id; + async _onApiGetOrCreatePopup(id, parentId) { + const popup = this.getOrCreatePopup(id, parentId); + return { + id: popup.id + }; } async _onApiSetOptions(id, options) { @@ -106,25 +143,10 @@ class PopupProxyHost { // Private functions - _createPopupInternal(parentId, depth) { - const parent = (typeof parentId === 'string' && this._popups.has(parentId) ? this._popups.get(parentId) : null); - const id = `${this._nextId}`; - if (parent !== null) { - depth = parent.depth + 1; - } - ++this._nextId; - const popup = new Popup(id, depth, this._frameIdPromise); - if (parent !== null) { - popup.setParent(parent); - } - this._popups.set(id, popup); - return {popup, id}; - } - _getPopup(id) { const popup = this._popups.get(id); if (typeof popup === 'undefined') { - throw new Error('Invalid popup ID'); + throw new Error(`Invalid popup ID ${id}`); } return popup; } diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index db6dffb1..093cdd2e 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -19,10 +19,10 @@ /*global FrontendApiSender*/ class PopupProxy { - constructor(depth, parentId, parentFrameId, url) { + constructor(id, depth, parentId, parentFrameId, url) { this._parentId = parentId; this._parentFrameId = parentFrameId; - this._id = null; + this._id = id; this._idPromise = null; this._depth = depth; this._url = url; @@ -113,7 +113,7 @@ class PopupProxy { } async _getPopupIdAsync() { - const id = await this._invokeHostApi('createNestedPopup', {parentId: this._parentId}); + const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId}); this._id = id; return id; } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 0b142dda..de05f9f5 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -28,8 +28,6 @@ class Popup { this._child = null; this._childrenSupported = true; this._injectPromise = null; - this._isInjected = false; - this._isInjectedAndLoaded = false; this._visible = false; this._visibleOverride = null; this._options = null; @@ -41,19 +39,28 @@ class Popup { this._container.className = 'yomichan-float'; this._container.addEventListener('mousedown', (e) => e.stopPropagation()); this._container.addEventListener('scroll', (e) => e.stopPropagation()); - this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); this._container.style.width = '0px'; this._container.style.height = '0px'; + this._fullscreenEventListeners = new EventListenerCollection(); + this._updateVisibility(); } // Public properties + get id() { + return this._id; + } + get parent() { return this._parent; } + get child() { + return this._child; + } + get depth() { return this._depth; } @@ -118,16 +125,12 @@ class Popup { } clearAutoPlayTimer() { - if (this._isInjectedAndLoaded) { - this._invokeApi('clearAutoPlayTimer'); - } + this._invokeApi('clearAutoPlayTimer'); } setContentScale(scale) { this._contentScale = scale; - if (this._isInjectedAndLoaded) { - this._invokeApi('setContentScale', {scale}); - } + this._invokeApi('setContentScale', {scale}); } // Popup-only public functions @@ -147,7 +150,7 @@ class Popup { } isVisibleSync() { - return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible); + return (this._visibleOverride !== null ? this._visibleOverride : this._visible); } updateTheme() { @@ -226,8 +229,10 @@ class Popup { return new Promise((resolve) => { const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); this._container.addEventListener('load', () => { - this._isInjectedAndLoaded = true; - this._invokeApi('initialize', { + const uniqueId = yomichan.generateId(32); + Popup._listenForDisplayPrepareCompleted(uniqueId, resolve); + + this._invokeApi('prepare', { options: this._options, popupInfo: { id: this._id, @@ -236,17 +241,47 @@ class Popup { }, url: this.url, childrenSupported: this._childrenSupported, - scale: this._contentScale + scale: this._contentScale, + uniqueId }); - resolve(); }); - this._observeFullscreen(); + this._observeFullscreen(true); this._onFullscreenChanged(); this.setCustomOuterCss(this._options.general.customPopupOuterCss, false); - this._isInjected = true; + this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); }); } + _observeFullscreen(observe) { + if (!observe) { + this._fullscreenEventListeners.removeAllEventListeners(); + return; + } + + if (this._fullscreenEventListeners.size > 0) { + // Already observing + return; + } + + const fullscreenEvents = [ + 'fullscreenchange', + 'MSFullscreenChange', + 'mozfullscreenchange', + 'webkitfullscreenchange' + ]; + const onFullscreenChanged = () => this._onFullscreenChanged(); + for (const eventName of fullscreenEvents) { + this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false); + } + } + + _onFullscreenChanged() { + const parent = (Popup._getFullscreenElement() || document.body || null); + if (parent !== null && this._container.parentNode !== parent) { + parent.appendChild(this._container); + } + } + async _show(elementRect, writingMode) { await this._inject(); @@ -328,38 +363,36 @@ class Popup { } _invokeApi(action, params={}) { - if (!this._isInjectedAndLoaded) { - throw new Error('Frame not loaded'); - } - this._container.contentWindow.postMessage({action, params}, '*'); - } - - _observeFullscreen() { - const fullscreenEvents = [ - 'fullscreenchange', - 'MSFullscreenChange', - 'mozfullscreenchange', - 'webkitfullscreenchange' - ]; - for (const eventName of fullscreenEvents) { - document.addEventListener(eventName, () => this._onFullscreenChanged(), false); + if (this._container.contentWindow) { + this._container.contentWindow.postMessage({action, params}, '*'); } } - _getFullscreenElement() { + static _getFullscreenElement() { return ( document.fullscreenElement || document.msFullscreenElement || document.mozFullScreenElement || - document.webkitFullscreenElement + document.webkitFullscreenElement || + null ); } - _onFullscreenChanged() { - const parent = (this._getFullscreenElement() || document.body || null); - if (parent !== null && this._container.parentNode !== parent) { - parent.appendChild(this._container); - } + static _listenForDisplayPrepareCompleted(uniqueId, resolve) { + const runtimeMessageCallback = ({action, params}, sender, callback) => { + if ( + action === 'popupPrepareCompleted' && + typeof params === 'object' && + params !== null && + params.uniqueId === uniqueId + ) { + chrome.runtime.onMessage.removeListener(runtimeMessageCallback); + callback(); + resolve(); + return false; + } + }; + chrome.runtime.onMessage.addListener(runtimeMessageCallback); } static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 11d3ff0e..fa785ec4 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -82,7 +82,11 @@ class TextSourceRange { } equals(other) { - if (other === null) { + if (!( + typeof other === 'object' && + other !== null && + other instanceof TextSourceRange + )) { return false; } if (this.imposterSourceElement !== null) { @@ -409,6 +413,12 @@ class TextSourceElement { } equals(other) { - return other && other.element === this.element && other.content === this.content; + return ( + typeof other === 'object' && + other !== null && + other instanceof TextSourceElement && + other.element === this.element && + other.content === this.content + ); } } diff --git a/ext/manifest.json b/ext/manifest.json index 79097674..68a8adb4 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -31,6 +31,7 @@ "fg/js/frontend-initialize.js" ], "css": ["fg/css/client.css"], + "match_about_blank": true, "all_frames": true }], "minimum_chrome_version": "57.0.0.0", diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 0b1e7e4f..86bdc73c 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -58,15 +58,15 @@ function apiDefinitionAdd(definition, mode, context, optionsContext) { } function apiDefinitionsAddable(definitions, modes, optionsContext) { - return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null); + return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}); } function apiNoteView(noteId) { return _apiInvoke('noteView', {noteId}); } -function apiTemplateRender(template, data, dynamic) { - return _apiInvoke('templateRender', {data, template, dynamic}); +function apiTemplateRender(template, data) { + return _apiInvoke('templateRender', {data, template}); } function apiAudioGetUrl(definition, source, optionsContext) { diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js index 76a3e7da..47db5c75 100644 --- a/ext/mixed/js/audio.js +++ b/ext/mixed/js/audio.js @@ -114,8 +114,11 @@ function audioGetFromUrl(url, willDownload) { async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) { const key = `${expression.expression}:${expression.reading}`; - if (cache !== null && hasOwn(cache, expression)) { - return cache[key]; + if (cache !== null) { + const cacheValue = cache.get(expression); + if (typeof cacheValue !== 'undefined') { + return cacheValue; + } } for (let i = 0, ii = sources.length; i < ii; ++i) { @@ -133,7 +136,7 @@ async function audioGetFromSources(expression, sources, optionsContext, willDown } const result = {audio, url, source}; if (cache !== null) { - cache[key] = result; + cache.set(key, result); } return result; } catch (e) { diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index ca9e98e5..330a30fb 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -113,11 +113,7 @@ function toIterable(value) { if (value !== null && typeof value === 'object') { const length = value.length; if (typeof length === 'number' && Number.isFinite(length)) { - const array = []; - for (let i = 0; i < length; ++i) { - array.push(value[i]); - } - return array; + return Array.from(value); } } @@ -240,6 +236,29 @@ class EventDispatcher { } } +class EventListenerCollection { + constructor() { + this._eventListeners = []; + } + + get size() { + return this._eventListeners.length; + } + + addEventListener(node, type, listener, options) { + node.addEventListener(type, listener, options); + this._eventListeners.push([node, type, listener, options]); + } + + removeAllEventListeners() { + if (this._eventListeners.length === 0) { return; } + for (const [node, type, listener, options] of this._eventListeners) { + node.removeEventListener(type, listener, options); + } + this._eventListeners = []; + } +} + /* * Default message handlers @@ -252,7 +271,7 @@ const yomichan = (() => { this._messageHandlers = new Map([ ['getUrl', this._onMessageGetUrl.bind(this)], - ['optionsUpdate', this._onMessageOptionsUpdate.bind(this)], + ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)], ['zoomChanged', this._onMessageZoomChanged.bind(this)] ]); @@ -261,6 +280,16 @@ const yomichan = (() => { // Public + generateId(length) { + const array = new Uint8Array(length); + window.crypto.getRandomValues(array); + let id = ''; + for (const value of array) { + id += value.toString(16).padStart(2, '0'); + } + return id; + } + triggerOrphaned(error) { this.trigger('orphaned', {error}); } @@ -280,8 +309,8 @@ const yomichan = (() => { return {url: window.location.href}; } - _onMessageOptionsUpdate({source}) { - this.trigger('optionsUpdate', {source}); + _onMessageOptionsUpdated({source}) { + this.trigger('optionsUpdated', {source}); } _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index 3617e546..46f3d17e 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -20,9 +20,6 @@ class DisplayGenerator { constructor() { - this._isInitialized = false; - this._initializationPromise = null; - this._termEntryTemplate = null; this._termExpressionTemplate = null; this._termDefinitionItemTemplate = null; @@ -41,18 +38,10 @@ class DisplayGenerator { this._tagFrequencyTemplate = null; } - isInitialized() { - return this._isInitialized; - } - - initialize() { - if (this._isInitialized) { - return Promise.resolve(); - } - if (this._initializationPromise === null) { - this._initializationPromise = this._initializeInternal(); - } - return this._initializationPromise; + async prepare() { + const html = await apiGetDisplayTemplatesHtml(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + this._setTemplates(doc); } createTermEntry(details) { @@ -304,13 +293,6 @@ class DisplayGenerator { return node; } - async _initializeInternal() { - const html = await apiGetDisplayTemplatesHtml(); - const doc = new DOMParser().parseFromString(html, 'text/html'); - this._setTemplates(doc); - this._isInitialized = true; - } - _setTemplates(doc) { this._termEntryTemplate = doc.querySelector('#term-entry-template'); this._termExpressionTemplate = doc.querySelector('#term-expression-template'); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index b18e275d..8113260c 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -32,11 +32,11 @@ class Display { this.index = 0; this.audioPlaying = null; this.audioFallback = null; - this.audioCache = {}; + this.audioCache = new Map(); this.styleNode = null; - this.eventListeners = []; - this.persistentEventListeners = []; + this.eventListeners = new EventListenerCollection(); + this.persistentEventListeners = new EventListenerCollection(); this.interactive = false; this.eventListenersActive = false; this.clickScanPrevent = false; @@ -48,6 +48,13 @@ class Display { this.setInteractive(true); } + async prepare(options=null) { + const displayGeneratorPromise = this.displayGenerator.prepare(); + const updateOptionsPromise = this.updateOptions(options); + await Promise.all([displayGeneratorPromise, updateOptionsPromise]); + yomichan.on('optionsUpdated', () => this.updateOptions(null)); + } + onError(_error) { throw new Error('Override me'); } @@ -179,13 +186,15 @@ class Display { e.preventDefault(); const link = e.currentTarget; const entry = link.closest('.entry'); - const definitionIndex = this.entryIndexFind(entry); + const index = this.entryIndexFind(entry); + if (index < 0 || index >= this.definitions.length) { return; } + const expressionIndex = Display.indexOf(entry.querySelectorAll('.term-expression .action-play-audio'), link); this.audioPlay( - this.definitions[definitionIndex], + this.definitions[index], // expressionIndex is used in audioPlay to detect result output mode Math.max(expressionIndex, this.options.general.resultOutputMode === 'merge' ? 0 : -1), - definitionIndex + index ); } @@ -193,6 +202,8 @@ class Display { e.preventDefault(); const link = e.currentTarget; const index = this.entryIndexFind(link); + if (index < 0 || index >= this.definitions.length) { return; } + this.noteAdd(this.definitions[index], link.dataset.mode); } @@ -243,15 +254,6 @@ class Display { throw new Error('Override me'); } - isInitialized() { - return this.options !== null; - } - - async initialize(options=null) { - await this.updateOptions(options); - yomichan.on('optionsUpdate', () => this.updateOptions(null)); - } - async updateOptions(options) { this.options = options ? options : await apiOptionsGet(this.getOptionsContext()); this.updateDocumentOptions(this.options); @@ -299,13 +301,23 @@ class Display { this.interactive = interactive; if (interactive) { - Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false); - Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false}); - Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-previous'), 'click', this.onSourceTermView.bind(this)); - Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-next'), 'click', this.onNextTermView.bind(this)); - Display.addEventListener(this.persistentEventListeners, document.querySelector('.navigation-header'), 'wheel', this.onHistoryWheel.bind(this), {passive: false}); + const actionPrevious = document.querySelector('.action-previous'); + const actionNext = document.querySelector('.action-next'); + const navigationHeader = document.querySelector('.navigation-header'); + + this.persistentEventListeners.addEventListener(document, 'keydown', this.onKeyDown.bind(this), false); + this.persistentEventListeners.addEventListener(document, 'wheel', this.onWheel.bind(this), {passive: false}); + if (actionPrevious !== null) { + this.persistentEventListeners.addEventListener(actionPrevious, 'click', this.onSourceTermView.bind(this)); + } + if (actionNext !== null) { + this.persistentEventListeners.addEventListener(actionNext, 'click', this.onNextTermView.bind(this)); + } + if (navigationHeader !== null) { + this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false}); + } } else { - Display.clearEventListeners(this.persistentEventListeners); + this.persistentEventListeners.removeAllEventListeners(); } this.setEventListenersActive(this.eventListenersActive); } @@ -316,23 +328,23 @@ class Display { this.eventListenersActive = active; if (active) { - this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this)); - this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this)); - this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this)); - this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this)); + this.addMultipleEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this)); + this.addMultipleEventListeners('.action-view-note', 'click', this.onNoteView.bind(this)); + this.addMultipleEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this)); + this.addMultipleEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this)); if (this.options.scanning.enablePopupSearch) { - this.addEventListeners('.term-glossary-item, .tag', 'mouseup', this.onGlossaryMouseUp.bind(this)); - this.addEventListeners('.term-glossary-item, .tag', 'mousedown', this.onGlossaryMouseDown.bind(this)); - this.addEventListeners('.term-glossary-item, .tag', 'mousemove', this.onGlossaryMouseMove.bind(this)); + this.addMultipleEventListeners('.term-glossary-item, .tag', 'mouseup', this.onGlossaryMouseUp.bind(this)); + this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this.onGlossaryMouseDown.bind(this)); + this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousemove', this.onGlossaryMouseMove.bind(this)); } } else { - Display.clearEventListeners(this.eventListeners); + this.eventListeners.removeAllEventListeners(); } } - addEventListeners(selector, type, listener, options) { + addMultipleEventListeners(selector, type, listener, options) { for (const node of this.container.querySelectorAll(selector)) { - Display.addEventListener(this.eventListeners, node, type, listener, options); + this.eventListeners.addEventListener(node, type, listener, options); } } @@ -362,7 +374,6 @@ class Display { async setContentTerms(definitions, context, token) { if (!context) { throw new Error('Context expected'); } - if (!this.isInitialized()) { return; } this.setEventListenersActive(false); @@ -370,11 +381,6 @@ class Display { window.focus(); } - if (!this.displayGenerator.isInitialized()) { - await this.displayGenerator.initialize(); - if (this.setContentToken !== token) { return; } - } - this.definitions = definitions; if (context.disableHistory) { delete context.disableHistory; @@ -418,7 +424,7 @@ class Display { this.setEventListenersActive(true); - const states = await apiDefinitionsAddable(definitions, ['term-kanji', 'term-kana'], this.getOptionsContext()); + const states = await this.getDefinitionsAddable(definitions, ['term-kanji', 'term-kana']); if (this.setContentToken !== token) { return; } this.updateAdderButtons(states); @@ -426,7 +432,6 @@ class Display { async setContentKanji(definitions, context, token) { if (!context) { throw new Error('Context expected'); } - if (!this.isInitialized()) { return; } this.setEventListenersActive(false); @@ -434,11 +439,6 @@ class Display { window.focus(); } - if (!this.displayGenerator.isInitialized()) { - await this.displayGenerator.initialize(); - if (this.setContentToken !== token) { return; } - } - this.definitions = definitions; if (context.disableHistory) { delete context.disableHistory; @@ -460,7 +460,7 @@ class Display { for (let i = 0, ii = definitions.length; i < ii; ++i) { if (i > 0) { - await promiseTimeout(0); + await promiseTimeout(1); if (this.setContentToken !== token) { return; } } @@ -473,7 +473,7 @@ class Display { this.setEventListenersActive(true); - const states = await apiDefinitionsAddable(definitions, ['kanji'], this.getOptionsContext()); + const states = await this.getDefinitionsAddable(definitions, ['kanji']); if (this.setContentToken !== token) { return; } this.updateAdderButtons(states); @@ -512,6 +512,8 @@ class Display { } autoPlayAudio() { + if (this.definitions.length === 0) { return; } + this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0); } @@ -611,9 +613,12 @@ class Display { } noteTryAdd(mode) { - const button = this.adderButtonFind(this.index, mode); + const index = this.index; + if (index < 0 || index >= this.definitions.length) { return; } + + const button = this.adderButtonFind(index, mode); if (button !== null && !button.classList.contains('disabled')) { - this.noteAdd(this.definitions[this.index], mode); + this.noteAdd(this.definitions[index], mode); } } @@ -712,7 +717,7 @@ class Display { async getScreenshot() { try { await this.setPopupVisibleOverride(false); - await Display.delay(1); // Wait for popup to be hidden. + await promiseTimeout(1); // Wait for popup to be hidden. const {format, quality} = this.options.anki.screenshot; const dataUrl = await apiScreenshotGet({format, quality}); @@ -781,8 +786,12 @@ class Display { return entry !== null ? entry.querySelector('.action-play-audio>img') : null; } - static delay(time) { - return new Promise((resolve) => setTimeout(resolve, time)); + async getDefinitionsAddable(definitions, modes) { + try { + return await apiDefinitionsAddable(definitions, modes, this.getOptionsContext()); + } catch (e) { + return []; + } } static indexOf(nodeList, node) { @@ -794,19 +803,6 @@ class Display { return -1; } - static addEventListener(eventListeners, object, type, listener, options) { - if (object === null) { return; } - object.addEventListener(type, listener, options); - eventListeners.push([object, type, listener, options]); - } - - static clearEventListeners(eventListeners) { - for (const [object, type, listener, options] of eventListeners) { - object.removeEventListener(type, listener, options); - } - eventListeners.length = 0; - } - static getElementTop(element) { const elementRect = element.getBoundingClientRect(); const documentRect = document.documentElement.getBoundingClientRect(); @@ -915,9 +911,12 @@ Display._onKeyDownHandlers = new Map([ ['P', (self, e) => { if (e.altKey) { - const entry = self.getEntry(self.index); + const index = self.index; + if (index < 0 || index >= self.definitions.length) { return; } + + const entry = self.getEntry(index); if (entry !== null && entry.dataset.type === 'term') { - self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index); + self.audioPlay(self.definitions[index], self.firstExpressionIndex, index); } return true; } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index e6da1e5e..aa10bbaf 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -31,7 +31,7 @@ class TextScanner { this.options = null; this.enabled = false; - this.eventListeners = []; + this.eventListeners = new EventListenerCollection(); this.primaryTouchIdentifier = null; this.preventNextContextMenu = false; @@ -229,7 +229,7 @@ class TextScanner { } } else { if (this.enabled) { - this.clearEventListeners(); + this.eventListeners.removeAllEventListeners(); this.enabled = false; } this.onSearchClear(false); @@ -237,13 +237,13 @@ class TextScanner { } hookEvents() { - let eventListeners = this.getMouseEventListeners(); + let eventListenerInfos = this.getMouseEventListeners(); if (this.options.scanning.touchInputEnabled) { - eventListeners = eventListeners.concat(this.getTouchEventListeners()); + eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners()); } - for (const [node, type, listener, options] of eventListeners) { - this.addEventListener(node, type, listener, options); + for (const [node, type, listener, options] of eventListenerInfos) { + this.eventListeners.addEventListener(node, type, listener, options); } } @@ -268,18 +268,6 @@ class TextScanner { ]; } - addEventListener(node, type, listener, options) { - node.addEventListener(type, listener, options); - this.eventListeners.push([node, type, listener, options]); - } - - clearEventListeners() { - for (const [node, type, listener, options] of this.eventListeners) { - node.removeEventListener(type, listener, options); - } - this.eventListeners = []; - } - setOptions(options) { this.options = options; this.setEnabled(this.options.general.enable); |