diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-07-03 15:58:29 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-03 15:58:29 -0400 | 
| commit | 8f48a23a458d5d72c9e8d0b5c79bd1eaa8e16925 (patch) | |
| tree | 803585c716a53178b0f8ca64622b9c680724d8f9 | |
| parent | e30bab33249a9a99b59a11f8ea582f31651b4710 (diff) | |
Display class refactoring (#650)
* Organize by public/private
* Don't access super class's private members
* Make _autoPlayAudioTimer private
* Refactor constructors
* Make functions private
* Organize by public/private
* Organize window message handlers
* Make fields private
* Refactor DisplaySearch constructor
* Make functions private
* Organize by public/private
| -rw-r--r-- | ext/bg/js/search.js | 299 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 170 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 205 | 
3 files changed, 350 insertions, 324 deletions
| diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index e9ec2b85..90e9fce8 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -30,31 +30,22 @@  class DisplaySearch extends Display {      constructor() {          super(document.querySelector('#spinner'), document.querySelector('#content')); -          this._isPrepared = false; - -        this.setOptionsContext({ -            depth: 0, -            url: window.location.href +        this._search = document.querySelector('#search'); +        this._query = document.querySelector('#query'); +        this._intro = document.querySelector('#intro'); +        this._clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable'); +        this._wanakanaEnable = document.querySelector('#wanakana-enable'); +        this._introVisible = true; +        this._introAnimationTimer = null; +        this._clipboardMonitor = new ClipboardMonitor({ +            getClipboard: api.clipboardGet.bind(api)          }); - -        this.queryParser = new QueryParser({ +        this._queryParser = new QueryParser({              getOptionsContext: this.getOptionsContext.bind(this),              setContent: this.setContent.bind(this),              setSpinnerVisible: this.setSpinnerVisible.bind(this)          }); - -        this.search = document.querySelector('#search'); -        this.query = document.querySelector('#query'); -        this.intro = document.querySelector('#intro'); -        this.clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable'); -        this.wanakanaEnable = document.querySelector('#wanakana-enable'); - -        this.introVisible = true; -        this.introAnimationTimer = null; - -        this.clipboardMonitor = new ClipboardMonitor({getClipboard: api.clipboardGet.bind(api)}); -          this._onKeyDownIgnoreKeys = new Map([              ['ANY_MOD', new Set([                  'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', @@ -69,17 +60,21 @@ class DisplaySearch extends Display {              ['AltGraph', new Set()],              ['Shift', new Set()]          ]); -          this._runtimeMessageHandlers = new Map([ -            ['searchQueryUpdate', this.onExternalSearchUpdate.bind(this)] +            ['searchQueryUpdate', this._onExternalSearchUpdate.bind(this)]          ]); + +        this.setOptionsContext({ +            depth: 0, +            url: window.location.href +        });      }      async prepare() {          await super.prepare();          await this.updateOptions();          yomichan.on('optionsUpdated', () => this.updateOptions()); -        await this.queryParser.prepare(); +        await this._queryParser.prepare();          const options = this.getOptions();          const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); @@ -87,35 +82,35 @@ class DisplaySearch extends Display {          document.documentElement.dataset.searchMode = mode;          if (options.general.enableWanakana === true) { -            this.wanakanaEnable.checked = true; -            wanakana.bind(this.query); +            this._wanakanaEnable.checked = true; +            wanakana.bind(this._query);          } else { -            this.wanakanaEnable.checked = false; +            this._wanakanaEnable.checked = false;          } -        this.setQuery(query); -        this.onSearchQueryUpdated(this.query.value, false); +        this._setQuery(query); +        this._onSearchQueryUpdated(this._query.value, false);          if (mode !== 'popup') {              if (options.general.enableClipboardMonitor === true) { -                this.clipboardMonitorEnable.checked = true; -                this.clipboardMonitor.start(); +                this._clipboardMonitorEnable.checked = true; +                this._clipboardMonitor.start();              } else { -                this.clipboardMonitorEnable.checked = false; +                this._clipboardMonitorEnable.checked = false;              } -            this.clipboardMonitorEnable.addEventListener('change', this.onClipboardMonitorEnableChange.bind(this)); +            this._clipboardMonitorEnable.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this));          } -        chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); +        chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); -        this.search.addEventListener('click', this.onSearch.bind(this), false); -        this.query.addEventListener('input', this.onSearchInput.bind(this), false); -        this.wanakanaEnable.addEventListener('change', this.onWanakanaEnableChange.bind(this)); -        window.addEventListener('popstate', this.onPopState.bind(this)); -        window.addEventListener('copy', this.onCopy.bind(this)); -        this.clipboardMonitor.on('change', this.onExternalSearchUpdate.bind(this)); +        this._search.addEventListener('click', this._onSearch.bind(this), false); +        this._query.addEventListener('input', this._onSearchInput.bind(this), false); +        this._wanakanaEnable.addEventListener('change', this._onWanakanaEnableChange.bind(this)); +        window.addEventListener('popstate', this._onPopState.bind(this)); +        window.addEventListener('copy', this._onCopy.bind(this)); +        this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this)); -        this.updateSearchButton(); +        this._updateSearchButton();          await this._prepareNestedPopups(); @@ -123,49 +118,94 @@ class DisplaySearch extends Display {      }      onEscape() { -        if (this.query === null) { +        if (this._query === null) {              return;          } -        this.query.focus(); -        this.query.select(); +        this._query.focus(); +        this._query.select();      } -    onSearchInput() { -        this.updateSearchButton(); +    onKeyDown(e) { +        const key = DOM.getKeyFromEvent(e); +        const ignoreKeys = this._onKeyDownIgnoreKeys; -        const queryElementRect = this.query.getBoundingClientRect(); +        const activeModifierMap = new Map([ +            ['Control', e.ctrlKey], +            ['Meta', e.metaKey], +            ['Shift', e.shiftKey], +            ['Alt', e.altKey], +            ['ANY_MOD', true] +        ]); + +        let preventFocus = false; +        for (const [modifier, keys] of ignoreKeys.entries()) { +            const modifierActive = activeModifierMap.get(modifier); +            if (key === modifier || (modifierActive && keys.has(key))) { +                preventFocus = true; +                break; +            } +        } + +        if (!super.onKeyDown(e) && !preventFocus && document.activeElement !== this._query) { +            this._query.focus({preventScroll: true}); +        } +    } + +    async updateOptions() { +        await super.updateOptions(); +        const options = this.getOptions(); +        this._queryParser.setOptions(options); +        if (!this._isPrepared) { return; } +        const query = this._query.value; +        if (query) { +            this._setQuery(query); +            this._onSearchQueryUpdated(query, false); +        } +    } + +    async setContent(type, details) { +        this._query.blur(); +        await super.setContent(type, details); +    } + +    // Private + +    _onSearchInput() { +        this._updateSearchButton(); + +        const queryElementRect = this._query.getBoundingClientRect();          if (queryElementRect.top < 0 || queryElementRect.bottom > window.innerHeight) { -            this.query.scrollIntoView(); +            this._query.scrollIntoView();          }      } -    onSearch(e) { -        if (this.query === null) { +    _onSearch(e) { +        if (this._query === null) {              return;          }          e.preventDefault(); -        const query = this.query.value; +        const query = this._query.value; -        this.queryParser.setText(query); +        this._queryParser.setText(query);          const url = new URL(window.location.href);          url.searchParams.set('query', query);          window.history.pushState(null, '', url.toString()); -        this.onSearchQueryUpdated(query, true); +        this._onSearchQueryUpdated(query, true);      } -    onPopState() { +    _onPopState() {          const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);          document.documentElement.dataset.searchMode = mode; -        this.setQuery(query); -        this.onSearchQueryUpdated(this.query.value, false); +        this._setQuery(query); +        this._onSearchQueryUpdated(this._query.value, false);      } -    onRuntimeMessage({action, params}, sender, callback) { +    _onRuntimeMessage({action, params}, sender, callback) {          const handler = this._runtimeMessageHandlers.get(action);          if (typeof handler !== 'function') { return false; } @@ -174,46 +214,20 @@ class DisplaySearch extends Display {          return false;      } -    onKeyDown(e) { -        const key = DOM.getKeyFromEvent(e); -        const ignoreKeys = this._onKeyDownIgnoreKeys; - -        const activeModifierMap = new Map([ -            ['Control', e.ctrlKey], -            ['Meta', e.metaKey], -            ['Shift', e.shiftKey], -            ['Alt', e.altKey], -            ['ANY_MOD', true] -        ]); - -        let preventFocus = false; -        for (const [modifier, keys] of ignoreKeys.entries()) { -            const modifierActive = activeModifierMap.get(modifier); -            if (key === modifier || (modifierActive && keys.has(key))) { -                preventFocus = true; -                break; -            } -        } - -        if (!super.onKeyDown(e) && !preventFocus && document.activeElement !== this.query) { -            this.query.focus({preventScroll: true}); -        } -    } - -    onCopy() { +    _onCopy() {          // ignore copy from search page -        this.clipboardMonitor.setPreviousText(window.getSelection().toString().trim()); +        this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim());      } -    onExternalSearchUpdate({text}) { -        this.setQuery(text); +    _onExternalSearchUpdate({text}) { +        this._setQuery(text);          const url = new URL(window.location.href);          url.searchParams.set('query', text);          window.history.pushState(null, '', url.toString()); -        this.onSearchQueryUpdated(this.query.value, true); +        this._onSearchQueryUpdated(this._query.value, true);      } -    async onSearchQueryUpdated(query, animate) { +    async _onSearchQueryUpdated(query, animate) {          try {              const details = {};              const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(query); @@ -227,8 +241,8 @@ class DisplaySearch extends Display {              }              const valid = (query.length > 0); -            this.setIntroVisible(!valid, animate); -            this.updateSearchButton(); +            this._setIntroVisible(!valid, animate); +            this._updateSearchButton();              if (valid) {                  const {definitions} = await api.termsFind(query, details, this.getOptionsContext());                  this.setContent('terms', {definitions, context: { @@ -240,19 +254,19 @@ class DisplaySearch extends Display {              } else {                  this.clearContent();              } -            this.setTitleText(query); +            this._setTitleText(query);              window.parent.postMessage('popupClose', '*');          } catch (e) {              this.onError(e);          }      } -    onWanakanaEnableChange(e) { +    _onWanakanaEnableChange(e) {          const value = e.target.checked;          if (value) { -            wanakana.bind(this.query); +            wanakana.bind(this._query);          } else { -            wanakana.unbind(this.query); +            wanakana.unbind(this._query);          }          api.modifySettings([{              action: 'set', @@ -263,13 +277,13 @@ class DisplaySearch extends Display {          }], 'search');      } -    onClipboardMonitorEnableChange(e) { +    _onClipboardMonitorEnableChange(e) {          if (e.target.checked) {              chrome.permissions.request(                  {permissions: ['clipboardRead']},                  (granted) => {                      if (granted) { -                        this.clipboardMonitor.start(); +                        this._clipboardMonitor.start();                          api.modifySettings([{                              action: 'set',                              path: 'general.enableClipboardMonitor', @@ -283,7 +297,7 @@ class DisplaySearch extends Display {                  }              );          } else { -            this.clipboardMonitor.stop(); +            this._clipboardMonitor.stop();              api.modifySettings([{                  action: 'set',                  path: 'general.enableClipboardMonitor', @@ -294,101 +308,84 @@ class DisplaySearch extends Display {          }      } -    async updateOptions() { -        await super.updateOptions(); -        const options = this.getOptions(); -        this.queryParser.setOptions(options); -        if (!this._isPrepared) { return; } -        const query = this.query.value; -        if (query) { -            this.setQuery(query); -            this.onSearchQueryUpdated(query, false); -        } -    } - -    isWanakanaEnabled() { -        return this.wanakanaEnable !== null && this.wanakanaEnable.checked; +    _isWanakanaEnabled() { +        return this._wanakanaEnable !== null && this._wanakanaEnable.checked;      } -    setQuery(query) { +    _setQuery(query) {          let interpretedQuery = query; -        if (this.isWanakanaEnabled()) { +        if (this._isWanakanaEnabled()) {              try {                  interpretedQuery = wanakana.toKana(query);              } catch (e) {                  // NOP              }          } -        this.query.value = interpretedQuery; -        this.queryParser.setText(interpretedQuery); -    } - -    async setContent(type, details) { -        this.query.blur(); -        await super.setContent(type, details); +        this._query.value = interpretedQuery; +        this._queryParser.setText(interpretedQuery);      } -    setIntroVisible(visible, animate) { -        if (this.introVisible === visible) { +    _setIntroVisible(visible, animate) { +        if (this._introVisible === visible) {              return;          } -        this.introVisible = visible; +        this._introVisible = visible; -        if (this.intro === null) { +        if (this._intro === null) {              return;          } -        if (this.introAnimationTimer !== null) { -            clearTimeout(this.introAnimationTimer); -            this.introAnimationTimer = null; +        if (this._introAnimationTimer !== null) { +            clearTimeout(this._introAnimationTimer); +            this._introAnimationTimer = null;          }          if (visible) { -            this.showIntro(animate); +            this._showIntro(animate);          } else { -            this.hideIntro(animate); +            this._hideIntro(animate);          }      } -    showIntro(animate) { +    _showIntro(animate) {          if (animate) {              const duration = 0.4; -            this.intro.style.transition = ''; -            this.intro.style.height = ''; -            const size = this.intro.getBoundingClientRect(); -            this.intro.style.height = '0px'; -            this.intro.style.transition = `height ${duration}s ease-in-out 0s`; -            window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation -            this.intro.style.height = `${size.height}px`; -            this.introAnimationTimer = setTimeout(() => { -                this.intro.style.height = ''; -                this.introAnimationTimer = null; +            this._intro.style.transition = ''; +            this._intro.style.height = ''; +            const size = this._intro.getBoundingClientRect(); +            this._intro.style.height = '0px'; +            this._intro.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this._intro).getPropertyValue('height'); // Commits height so next line can start animation +            this._intro.style.height = `${size.height}px`; +            this._introAnimationTimer = setTimeout(() => { +                this._intro.style.height = ''; +                this._introAnimationTimer = null;              }, duration * 1000);          } else { -            this.intro.style.transition = ''; -            this.intro.style.height = ''; +            this._intro.style.transition = ''; +            this._intro.style.height = '';          }      } -    hideIntro(animate) { +    _hideIntro(animate) {          if (animate) {              const duration = 0.4; -            const size = this.intro.getBoundingClientRect(); -            this.intro.style.height = `${size.height}px`; -            this.intro.style.transition = `height ${duration}s ease-in-out 0s`; -            window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation +            const size = this._intro.getBoundingClientRect(); +            this._intro.style.height = `${size.height}px`; +            this._intro.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this._intro).getPropertyValue('height'); // Commits height so next line can start animation          } else { -            this.intro.style.transition = ''; +            this._intro.style.transition = '';          } -        this.intro.style.height = '0'; +        this._intro.style.height = '0';      } -    updateSearchButton() { -        this.search.disabled = this.introVisible && (this.query === null || this.query.value.length === 0); +    _updateSearchButton() { +        this._search.disabled = this._introVisible && (this._query === null || this._query.value.length === 0);      } -    setTitleText(text) { +    _setTitleText(text) {          // Chrome limits title to 1024 characters          if (text.length > 1000) {              text = text.substring(0, 1000) + '...'; diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index d86f16c3..83355c5c 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -26,39 +26,35 @@  class DisplayFloat extends Display {      constructor() {          super(document.querySelector('#spinner'), document.querySelector('#definitions')); -        this.autoPlayAudioTimer = null; - +        this._autoPlayAudioTimer = null;          this._secret = yomichan.generateId(16);          this._token = null; -          this._nestedPopupsPrepared = false; +        this._windowMessageHandlers = new Map([ +            ['initialize',         {handler: this._onMessageInitialize.bind(this), authenticate: false}], +            ['configure',          {handler: this._onMessageConfigure.bind(this)}], +            ['setOptionsContext',  {handler: this._onMessageSetOptionsContext.bind(this)}], +            ['setContent',         {handler: this._onMessageSetContent.bind(this)}], +            ['clearAutoPlayTimer', {handler: this._onMessageClearAutoPlayTimer.bind(this)}], +            ['setCustomCss',       {handler: this._onMessageSetCustomCss.bind(this)}], +            ['setContentScale',    {handler: this._onMessageSetContentScale.bind(this)}] +        ]); -        this._onKeyDownHandlers = new Map([ +        this.setOnKeyDownHandlers([              ['C', (e) => {                  if (e.ctrlKey && !window.getSelection().toString()) { -                    this.onSelectionCopy(); +                    this._copySelection();                      return true;                  }                  return false; -            }], -            ...this._onKeyDownHandlers -        ]); - -        this._windowMessageHandlers = new Map([ -            ['initialize', {handler: this._initialize.bind(this), authenticate: false}], -            ['configure', {handler: this._configure.bind(this)}], -            ['setOptionsContext', {handler: ({optionsContext}) => this.setOptionsContext(optionsContext)}], -            ['setContent', {handler: ({type, details}) => this.setContent(type, details)}], -            ['clearAutoPlayTimer', {handler: () => this.clearAutoPlayTimer()}], -            ['setCustomCss', {handler: ({css}) => this.setCustomCss(css)}], -            ['setContentScale', {handler: ({scale}) => this.setContentScale(scale)}] +            }]          ]);      }      async prepare() {          await super.prepare(); -        window.addEventListener('message', this.onMessage.bind(this), false); +        window.addEventListener('message', this._onMessage.bind(this), false);          api.broadcastTab('popupPrepared', {secret: this._secret});      } @@ -67,11 +63,46 @@ class DisplayFloat extends Display {          window.parent.postMessage('popupClose', '*');      } -    onSelectionCopy() { -        window.parent.postMessage('selectionCopy', '*'); +    async setOptionsContext(optionsContext) { +        super.setOptionsContext(optionsContext); +        await this.updateOptions();      } -    onMessage(e) { +    async getDocumentTitle() { +        try { +            const uniqueId = yomichan.generateId(16); + +            const promise = yomichan.getTemporaryListenerResult( +                chrome.runtime.onMessage, +                ({action, params}, {resolve}) => { +                    if ( +                        action === 'documentInformationBroadcast' && +                        isObject(params) && +                        params.uniqueId === uniqueId && +                        params.frameId === 0 +                    ) { +                        resolve(params); +                    } +                }, +                2000 +            ); +            api.broadcastTab('requestDocumentInformationBroadcast', {uniqueId}); + +            const {title} = await promise; +            return title; +        } catch (e) { +            return ''; +        } +    } + +    autoPlayAudio() { +        this._clearAutoPlayTimer(); +        this._autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400); +    } + +    // Message handling + +    _onMessage(e) {          const data = e.data;          if (typeof data !== 'object' || data === null) {              this._logMessageError(e, 'Invalid data'); @@ -99,56 +130,65 @@ class DisplayFloat extends Display {          handler(data.params);      } -    autoPlayAudio() { -        this.clearAutoPlayTimer(); -        this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400); +    _onMessageInitialize(params) { +        this._initialize(params);      } -    clearAutoPlayTimer() { -        if (this.autoPlayAudioTimer) { -            window.clearTimeout(this.autoPlayAudioTimer); -            this.autoPlayAudioTimer = null; +    async _onMessageConfigure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) { +        this.setOptionsContext(optionsContext); + +        await this.updateOptions(); + +        if (childrenSupported && !this._nestedPopupsPrepared) { +            const {depth, url} = optionsContext; +            this._prepareNestedPopups(popupId, depth, frameId, url); +            this._nestedPopupsPrepared = true;          } + +        this._setContentScale(scale); + +        api.sendMessageToFrame(frameId, 'popupConfigured', {messageId});      } -    async setOptionsContext(optionsContext) { -        super.setOptionsContext(optionsContext); -        await this.updateOptions(); +    _onMessageSetOptionsContext({optionsContext}) { +        this.setOptionsContext(optionsContext);      } -    setContentScale(scale) { -        const body = document.body; -        if (body === null) { return; } -        body.style.fontSize = `${scale}em`; +    _onMessageSetContent({type, details}) { +        this.setContent(type, details);      } -    async getDocumentTitle() { -        try { -            const uniqueId = yomichan.generateId(16); +    _onMessageClearAutoPlayTimer() { +        this._clearAutoPlayTimer.bind(this); +    } -            const promise = yomichan.getTemporaryListenerResult( -                chrome.runtime.onMessage, -                ({action, params}, {resolve}) => { -                    if ( -                        action === 'documentInformationBroadcast' && -                        isObject(params) && -                        params.uniqueId === uniqueId && -                        params.frameId === 0 -                    ) { -                        resolve(params); -                    } -                }, -                2000 -            ); -            api.broadcastTab('requestDocumentInformationBroadcast', {uniqueId}); +    _onMessageSetCustomCss({css}) { +        this.setCustomCss(css); +    } -            const {title} = await promise; -            return title; -        } catch (e) { -            return ''; +    _onMessageSetContentScale({scale}) { +        this._setContentScale(scale); +    } + +    // Private + +    _copySelection() { +        window.parent.postMessage('selectionCopy', '*'); +    } + +    _clearAutoPlayTimer() { +        if (this._autoPlayAudioTimer) { +            window.clearTimeout(this._autoPlayAudioTimer); +            this._autoPlayAudioTimer = null;          }      } +    _setContentScale(scale) { +        const body = document.body; +        if (body === null) { return; } +        body.style.fontSize = `${scale}em`; +    } +      _logMessageError(event, type) {          yomichan.logWarning(new Error(`Popup received invalid message from origin ${JSON.stringify(event.origin)}: ${type}`));      } @@ -166,22 +206,6 @@ class DisplayFloat extends Display {          api.sendMessageToFrame(frameId, 'popupInitialized', {secret, token});      } -    async _configure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) { -        this.setOptionsContext(optionsContext); - -        await this.updateOptions(); - -        if (childrenSupported && !this._nestedPopupsPrepared) { -            const {depth, url} = optionsContext; -            this._prepareNestedPopups(popupId, depth, frameId, url); -            this._nestedPopupsPrepared = true; -        } - -        this.setContentScale(scale); - -        api.sendMessageToFrame(frameId, 'popupConfigured', {messageId}); -    } -      _isMessageAuthenticated(message) {          return (              this._token !== null && diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index f02a6e5c..2ab1b871 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -47,18 +47,15 @@ class Display {              useCache: true          });          this._styleNode = null; -          this._eventListeners = new EventListenerCollection();          this._persistentEventListeners = new EventListenerCollection();          this._interactive = false;          this._eventListenersActive = false;          this._clickScanPrevent = false;          this._setContentToken = null; -          this._mediaLoader = new MediaLoader();          this._displayGenerator = new DisplayGenerator({mediaLoader: this._mediaLoader});          this._windowScroll = new WindowScroll(); -          this._onKeyDownHandlers = new Map([              ['Escape', () => {                  this.onEscape(); @@ -182,6 +179,111 @@ class Display {          throw new Error('Override me');      } +    onKeyDown(e) { +        const key = DOM.getKeyFromEvent(e); +        const handler = this._onKeyDownHandlers.get(key); +        if (typeof handler === 'function') { +            if (handler(e)) { +                e.preventDefault(); +                return true; +            } +        } +        return false; +    } + +    getOptions() { +        return this._options; +    } + +    getOptionsContext() { +        return this._optionsContext; +    } + +    setOptionsContext(optionsContext) { +        this._optionsContext = optionsContext; +    } + +    async updateOptions() { +        this._options = await api.optionsGet(this.getOptionsContext()); +        this._updateDocumentOptions(this._options); +        this._updateTheme(this._options.general.popupTheme); +        this.setCustomCss(this._options.general.customPopupCss); +    } + +    addMultipleEventListeners(selector, type, listener, options) { +        for (const node of this._container.querySelectorAll(selector)) { +            this._eventListeners.addEventListener(node, type, listener, options); +        } +    } + +    autoPlayAudio() { +        if (this._definitions.length === 0) { return; } + +        this._audioPlay(this._definitions[0], this._getFirstExpressionIndex(), 0); +    } + +    async setContent(type, details) { +        const token = {}; // Unique identifier token +        this._setContentToken = token; +        try { +            this._mediaLoader.unloadAll(); + +            switch (type) { +                case 'terms': +                    await this._setContentTerms(details.definitions, details.context, token); +                    break; +                case 'kanji': +                    await this._setContentKanji(details.definitions, details.context, token); +                    break; +                case 'extensionUnloaded': +                    this._setContentExtensionUnloaded(); +                    break; +            } +        } catch (e) { +            this.onError(e); +        } finally { +            if (this._setContentToken === token) { +                this._setContentToken = null; +            } +        } +    } + +    clearContent() { +        this._container.textContent = ''; +    } + +    setCustomCss(css) { +        if (this._styleNode === null) { +            if (css.length === 0) { return; } +            this._styleNode = document.createElement('style'); +        } + +        this._styleNode.textContent = css; + +        const parent = document.head; +        if (this._styleNode.parentNode !== parent) { +            parent.appendChild(this._styleNode); +        } +    } + +    async getDocumentTitle() { +        return document.title; +    } + +    setSpinnerVisible(visible) { +        if (this._spinner !== null) { +            this._spinner.hidden = !visible; +        } +    } + +    setOnKeyDownHandlers(handlers) { +        for (const [key, handler] of handlers) { +            this._onKeyDownHandlers.set(key, handler); +        } +    } + +    // Private +      _onSourceTermView(e) {          e.preventDefault();          this._sourceTermView(); @@ -335,18 +437,6 @@ class Display {          api.noteView(link.dataset.noteId);      } -    onKeyDown(e) { -        const key = DOM.getKeyFromEvent(e); -        const handler = this._onKeyDownHandlers.get(key); -        if (typeof handler === 'function') { -            if (handler(e)) { -                e.preventDefault(); -                return true; -            } -        } -        return false; -    } -      _onWheel(e) {          if (e.altKey) {              if (e.deltaY !== 0) { @@ -372,25 +462,6 @@ class Display {          }      } -    getOptions() { -        return this._options; -    } - -    getOptionsContext() { -        return this._optionsContext; -    } - -    setOptionsContext(optionsContext) { -        this._optionsContext = optionsContext; -    } - -    async updateOptions() { -        this._options = await api.optionsGet(this.getOptionsContext()); -        this._updateDocumentOptions(this._options); -        this._updateTheme(this._options.general.popupTheme); -        this.setCustomCss(this._options.general.customPopupCss); -    } -      _updateDocumentOptions(options) {          const data = document.documentElement.dataset;          data.ankiEnabled = `${options.anki.enable}`; @@ -407,20 +478,6 @@ class Display {          document.documentElement.dataset.yomichanTheme = themeName;      } -    setCustomCss(css) { -        if (this._styleNode === null) { -            if (css.length === 0) { return; } -            this._styleNode = document.createElement('style'); -        } - -        this._styleNode.textContent = css; - -        const parent = document.head; -        if (this._styleNode.parentNode !== parent) { -            parent.appendChild(this._styleNode); -        } -    } -      _setInteractive(interactive) {          interactive = !!interactive;          if (this._interactive === interactive) { return; } @@ -469,38 +526,6 @@ class Display {          }      } -    addMultipleEventListeners(selector, type, listener, options) { -        for (const node of this._container.querySelectorAll(selector)) { -            this._eventListeners.addEventListener(node, type, listener, options); -        } -    } - -    async setContent(type, details) { -        const token = {}; // Unique identifier token -        this._setContentToken = token; -        try { -            this._mediaLoader.unloadAll(); - -            switch (type) { -                case 'terms': -                    await this._setContentTerms(details.definitions, details.context, token); -                    break; -                case 'kanji': -                    await this._setContentKanji(details.definitions, details.context, token); -                    break; -                case 'extensionUnloaded': -                    this._setContentExtensionUnloaded(); -                    break; -            } -        } catch (e) { -            this.onError(e); -        } finally { -            if (this._setContentToken === token) { -                this._setContentToken = null; -            } -        } -    } -      async _setContentTerms(definitions, context, token) {          if (!context) { throw new Error('Context expected'); } @@ -631,10 +656,6 @@ class Display {          }      } -    clearContent() { -        this._container.textContent = ''; -    } -      _updateNavigation(previous, next) {          const navigation = document.querySelector('#navigation-header');          if (navigation !== null) { @@ -644,12 +665,6 @@ class Display {          }      } -    autoPlayAudio() { -        if (this._definitions.length === 0) { return; } - -        this._audioPlay(this._definitions[0], this._getFirstExpressionIndex(), 0); -    } -      _updateAdderButtons(states) {          for (let i = 0; i < states.length; ++i) {              let noteId = null; @@ -885,12 +900,6 @@ class Display {          return api.broadcastTab('popupSetVisibleOverride', {visible});      } -    setSpinnerVisible(visible) { -        if (this._spinner !== null) { -            this._spinner.hidden = !visible; -        } -    } -      _getEntry(index) {          const entries = this._container.querySelectorAll('.entry');          return index >= 0 && index < entries.length ? entries[index] : null; @@ -950,10 +959,6 @@ class Display {          }      } -    async getDocumentTitle() { -        return document.title; -    } -      _indexOf(nodeList, node) {          for (let i = 0, ii = nodeList.length; i < ii; ++i) {              if (nodeList[i] === node) { |