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); |