diff options
| -rw-r--r-- | ext/bg/js/search.js | 123 | ||||
| -rw-r--r-- | ext/bg/search.html | 2 | ||||
| -rw-r--r-- | ext/fg/float.html | 2 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 2 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 13 | ||||
| -rw-r--r-- | ext/mixed/js/display-context.js | 55 | ||||
| -rw-r--r-- | ext/mixed/js/display-history.js | 178 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 316 | 
8 files changed, 472 insertions, 219 deletions
| diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index c29abbaa..c5051fa9 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -33,6 +33,7 @@ class DisplaySearch extends Display {          this._intro = document.querySelector('#intro');          this._clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable');          this._wanakanaEnable = document.querySelector('#wanakana-enable'); +        this._queryText = '';          this._introVisible = true;          this._introAnimationTimer = null;          this._clipboardMonitor = new ClipboardMonitor({ @@ -68,6 +69,9 @@ class DisplaySearch extends Display {          await this._queryParser.prepare();          this._queryParser.on('searched', this._onQueryParserSearch.bind(this)); +        this.on('contentUpdating', this._onContentUpdating.bind(this)); + +        this.setHistorySettings({useBrowserHistory: true});          const options = this.getOptions(); @@ -83,7 +87,6 @@ class DisplaySearch extends Display {          }          this._setQuery(query); -        this._onSearchQueryUpdated(this._query.value, false);          if (mode !== 'popup') {              if (options.general.enableClipboardMonitor === true) { @@ -100,7 +103,6 @@ class DisplaySearch extends Display {          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)); @@ -108,6 +110,8 @@ class DisplaySearch extends Display {          await this._prepareNestedPopups(); +        this.initializeState(); +          this._isPrepared = true;      } @@ -158,29 +162,47 @@ class DisplaySearch extends Display {          }      } -    async setContent(...args) { -        this._query.blur(); -        this._closePopups(); -        return await super.setContent(...args); -    } +    // Private -    clearContent() { +    _onContentUpdating({type, source, content}) { +        let animate = false; +        let valid = false; +        switch (type) { +            case 'terms': +            case 'kanji': +                animate = content.animate; +                valid = content.definitions.length > 0; +                this._query.blur(); +                break; +            case 'clear': +                valid = false; +                animate = true; +                source = ''; +                break; +        } +        if (typeof source !== 'string') { source = ''; }          this._closePopups(); -        return super.clearContent(); +        this._setQuery(source); +        this._setIntroVisible(!valid, animate); +        this._setTitleText(source); +        this._updateSearchButton();      } -    // Private -      _onQueryParserSearch({type, definitions, sentence, cause, textSource}) {          this.setContent({              focus: false,              history: cause !== 'mouse', -            type, -            source: textSource.text(), -            definitions, -            context: { +            params: { +                type, +                query: textSource.text(), +                wildcards: 'off' +            }, +            state: {                  sentence,                  url: window.location.href +            }, +            content: { +                definitions              }          });      } @@ -202,22 +224,9 @@ class DisplaySearch extends Display {          e.preventDefault();          const query = this._query.value; - -        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);      } -    _onPopState() { -        const {queryParams: {query=''}} = parseUrl(window.location.href); -        this._setQuery(query); -        this._onSearchQueryUpdated(this._query.value, false); -    } -      _onRuntimeMessage({action, params}, sender, callback) {          const messageHandler = this._runtimeMessageHandlers.get(action);          if (typeof messageHandler === 'undefined') { return false; } @@ -230,49 +239,25 @@ class DisplaySearch extends Display {      }      _onExternalSearchUpdate({text, animate=true}) { -        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, animate); +        this._onSearchQueryUpdated(text, animate);      } -    async _onSearchQueryUpdated(query, animate) { -        try { -            const details = {}; -            const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(query); -            if (match !== null) { -                if (match[1]) { -                    details.wildcard = 'prefix'; -                } else if (match[3]) { -                    details.wildcard = 'suffix'; -                } -                query = match[2]; -            } - -            const valid = (query.length > 0); -            this._setIntroVisible(!valid, animate); -            this._updateSearchButton(); -            if (valid) { -                const {definitions} = await api.termsFind(query, details, this.getOptionsContext()); -                this.setContent({ -                    focus: false, -                    history: false, -                    definitions, -                    source: query, -                    type: 'terms', -                    context: { -                        sentence: {text: query, offset: 0}, -                        url: window.location.href -                    } -                }); -            } else { -                this.clearContent(); +    _onSearchQueryUpdated(query, animate) { +        this.setContent({ +            focus: false, +            history: false, +            params: { +                query +            }, +            state: { +                sentence: {text: query, offset: 0}, +                url: window.location.href +            }, +            content: { +                definitions: null, +                animate              } -            this._setTitleText(query); -        } catch (e) { -            this.onError(e); -        } +        });      }      _onWanakanaEnableChange(e) { @@ -335,6 +320,8 @@ class DisplaySearch extends Display {                  // NOP              }          } +        if (this._queryText === interpretedQuery) { return; } +        this._queryText = interpretedQuery;          this._query.value = interpretedQuery;          this._queryParser.setText(interpretedQuery);      } diff --git a/ext/bg/search.html b/ext/bg/search.html index 8f7c1d4c..dd44b376 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -83,9 +83,9 @@          <script src="/fg/js/dom-text-scanner.js"></script>          <script src="/fg/js/source.js"></script>          <script src="/mixed/js/audio-system.js"></script> -        <script src="/mixed/js/display-context.js"></script>          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/display-generator.js"></script> +        <script src="/mixed/js/display-history.js"></script>          <script src="/mixed/js/dynamic-loader.js"></script>          <script src="/mixed/js/media-loader.js"></script>          <script src="/mixed/js/scroll.js"></script> diff --git a/ext/fg/float.html b/ext/fg/float.html index 9e0e9ff4..f5a85f8e 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -50,9 +50,9 @@          <script src="/fg/js/dom-text-scanner.js"></script>          <script src="/fg/js/source.js"></script>          <script src="/mixed/js/audio-system.js"></script> -        <script src="/mixed/js/display-context.js"></script>          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/display-generator.js"></script> +        <script src="/mixed/js/display-history.js"></script>          <script src="/mixed/js/dynamic-loader.js"></script>          <script src="/mixed/js/frame-endpoint.js"></script>          <script src="/mixed/js/media-loader.js"></script> diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index f23a9b93..513ee178 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -50,6 +50,8 @@ class DisplayFloat extends Display {          ]);          window.addEventListener('message', this._onWindowMessage.bind(this), false); +        this.initializeState(); +          this._frameEndpoint.signal();      } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 73cea841..09928cd4 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -429,12 +429,17 @@ class Frontend {              {                  focus,                  history: false, -                type, -                source: textSource.text(), -                definitions, -                context: { +                params: { +                    type, +                    query: textSource.text(), +                    wildcards: 'off' +                }, +                state: {                      sentence,                      url +                }, +                content: { +                    definitions                  }              }          ); diff --git a/ext/mixed/js/display-context.js b/ext/mixed/js/display-context.js deleted file mode 100644 index 2322974a..00000000 --- a/ext/mixed/js/display-context.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2019-2020  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - - -class DisplayContext { -    constructor(type, source, definitions, context) { -        this.type = type; -        this.source = source; -        this.definitions = definitions; -        this.context = context; -    } - -    get(key) { -        return this.context[key]; -    } - -    set(key, value) { -        this.context[key] = value; -    } - -    update(data) { -        Object.assign(this.context, data); -    } - -    get previous() { -        return this.context.previous; -    } - -    get next() { -        return this.context.next; -    } - -    static push(self, type, source, definitions, context) { -        const newContext = new DisplayContext(type, source, definitions, context); -        if (self !== null) { -            newContext.update({previous: self}); -            self.update({next: newContext}); -        } -        return newContext; -    } -} diff --git a/ext/mixed/js/display-history.js b/ext/mixed/js/display-history.js new file mode 100644 index 00000000..cf2db8d5 --- /dev/null +++ b/ext/mixed/js/display-history.js @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2020  Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +class DisplayHistory extends EventDispatcher { +    constructor({clearable=true, useBrowserHistory=false}) { +        super(); +        this._clearable = clearable; +        this._useBrowserHistory = useBrowserHistory; +        this._historyMap = new Map(); + +        const historyState = history.state; +        const {id, state} = isObject(historyState) ? historyState : {id: null, state: null}; +        this._current = this._createHistoryEntry(id, location.href, state, null, null); +    } + +    get state() { +        return this._current.state; +    } + +    get content() { +        return this._current.content; +    } + +    get useBrowserHistory() { +        return this._useBrowserHistory; +    } + +    set useBrowserHistory(value) { +        this._useBrowserHistory = value; +    } + +    prepare() { +        window.addEventListener('popstate', this._onPopState.bind(this), false); +    } + +    hasNext() { +        return this._current.next !== null; +    } + +    hasPrevious() { +        return this._current.previous !== null; +    } + +    clear() { +        if (!this._clearable) { return; } +        this._clear(); +    } + +    back() { +        return this._go(false); +    } + +    forward() { +        return this._go(true); +    } + +    pushState(state, content, url) { +        if (typeof url === 'undefined') { url = location.href; } + +        const entry = this._createHistoryEntry(null, url, state, content, this._current); +        this._current.next = entry; +        this._current = entry; +        this._updateHistoryFromCurrent(!this._useBrowserHistory); +    } + +    replaceState(state, content, url) { +        if (typeof url === 'undefined') { url = location.href; } + +        this._current.url = url; +        this._current.state = state; +        this._current.content = content; +        this._updateHistoryFromCurrent(true); +    } + +    _onPopState() { +        this._updateStateFromHistory(); +        this._triggerStateChanged(false); +    } + +    _go(forward) { +        const target = forward ? this._current.next : this._current.previous; +        if (target === null) { +            return false; +        } + +        if (this._useBrowserHistory) { +            if (forward) { +                history.forward(); +            } else { +                history.back(); +            } +        } else { +            this._current = target; +            this._updateHistoryFromCurrent(true); +        } + +        return true; +    } + +    _triggerStateChanged(synthetic) { +        this.trigger('stateChanged', {history: this, synthetic}); +    } + +    _updateHistoryFromCurrent(replace) { +        const {id, state, url} = this._current; +        if (replace) { +            history.replaceState({id, state}, '', url); +        } else { +            history.pushState({id, state}, '', url); +        } +        this._triggerStateChanged(true); +    } + +    _updateStateFromHistory() { +        let state = history.state; +        let id = null; +        if (isObject(state)) { +            id = state.id; +            if (typeof id === 'string') { +                const entry = this._historyMap.get(id); +                if (typeof entry !== 'undefined') { +                    // Valid +                    this._current = entry; +                    return; +                } +            } +            // Partial state recovery +            state = state.state; +        } else { +            state = null; +        } + +        // Fallback +        this._current.id = (typeof id === 'string' ? id : this._generateId()); +        this._current.state = state; +        this._current.content = null; +        this._clear(); +    } + +    _createHistoryEntry(id, url, state, content, previous) { +        if (typeof id !== 'string') { id = this._generateId(); } +        const entry = { +            id, +            url, +            next: null, +            previous, +            state, +            content +        }; +        this._historyMap.set(id, entry); +        return entry; +    } + +    _generateId() { +        return yomichan.generateId(16); +    } + +    _clear() { +        this._historyMap.clear(); +        this._historyMap.set(this._current.id, this._current); +        this._current.next = null; +        this._current.previous = null; +    } +} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index b2d7d54d..e78b9765 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -18,8 +18,8 @@  /* global   * AudioSystem   * DOM - * DisplayContext   * DisplayGenerator + * DisplayHistory   * Frontend   * MediaLoader   * PopupFactory @@ -30,14 +30,14 @@   * dynamicLoader   */ -class Display { +class Display extends EventDispatcher {      constructor(spinner, container) { +        super();          this._spinner = spinner;          this._container = container;          this._definitions = [];          this._optionsContext = {depth: 0, url: window.location.href};          this._options = null; -        this._context = null;          this._index = 0;          this._audioPlaying = null;          this._audioFallback = null; @@ -64,6 +64,9 @@ class Display {          this._hotkeys = new Map();          this._actions = new Map();          this._messageHandlers = new Map(); +        this._history = new DisplayHistory({clearable: true, useBrowserHistory: false}); +        this._historyChangeIgnore = false; +        this._historyHasChanged = false;          this.registerActions([              ['close',               () => { this.onEscape(); }], @@ -116,12 +119,27 @@ class Display {      async prepare() {          this._setInteractive(true);          await this._displayGenerator.prepare(); +        this._history.prepare(); +        this._history.on('stateChanged', this._onStateChanged.bind(this));          yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));          api.crossFrame.registerHandlers([              ['popupMessage', {async: 'dynamic', handler: this._onMessage.bind(this)}]          ]);      } +    initializeState() { +        this._onStateChanged(); +    } + +    setHistorySettings({clearable, useBrowserHistory}) { +        if (typeof clearable !== 'undefined') { +            this._history.clearable = clearable; +        } +        if (typeof useBrowserHistory !== 'undefined') { +            this._history.useBrowserHistory = useBrowserHistory; +        } +    } +      onError(error) {          if (yomichan.isExtensionUnloaded) { return; }          yomichan.logError(error); @@ -202,46 +220,25 @@ class Display {          }      } -    async setContent(details) { -        const token = {}; // Unique identifier token -        this._setContentToken = token; -        try { -            this._mediaLoader.unloadAll(); - -            const {focus, history, type, source, definitions, context} = details; - -            if (!history) { -                this._context = new DisplayContext(type, source, definitions, context); -            } else { -                this._context = DisplayContext.push(this._context, type, source, definitions, context); -            } +    setContent(details) { +        const {focus, history, params, state, content} = details; -            if (focus !== false) { -                window.focus(); -            } +        if (focus) { +            window.focus(); +        } -            switch (type) { -                case 'terms': -                case 'kanji': -                    { -                        const {sentence, url, index=0, scroll=null} = context; -                        await this._setContentTermsOrKanji((type === 'terms'), definitions, sentence, url, index, scroll, token); -                    } -                    break; -            } -        } catch (e) { -            this.onError(e); -        } finally { -            if (this._setContentToken === token) { -                this._setContentToken = null; -            } +        const urlSearchParams = new URLSearchParams(); +        for (const [key, value] of Object.entries(params)) { +            urlSearchParams.append(key, value);          } -    } +        const url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`; -    clearContent() { -        this._setEventListenersActive(false); -        this._container.textContent = ''; -        this._setEventListenersActive(true); +        if (history && this._historyHasChanged) { +            this._history.pushState(state, content, url); +        } else { +            this._history.clear(); +            this._history.replaceState(state, content, url); +        }      }      setCustomCss(css) { @@ -348,8 +345,95 @@ class Display {      // Private +    async _onStateChanged() { +        if (this._historyChangeIgnore) { return; } + +        const token = {}; // Unique identifier token +        this._setContentToken = token; +        try { +            const urlSearchParams = new URLSearchParams(location.search); +            let type = urlSearchParams.get('type'); +            if (type === null) { type = 'terms'; } + +            let asigned = false; +            const eventArgs = {type, urlSearchParams, token}; +            this._historyHasChanged = true; +            this._mediaLoader.unloadAll(); +            switch (type) { +                case 'terms': +                case 'kanji': +                    { +                        const source = urlSearchParams.get('query'); +                        if (!source) { break; } + +                        const isTerms = (type === 'terms'); +                        let {state, content} = this._history; +                        let changeHistory = false; +                        if (!isObject(content)) { +                            content = {}; +                            changeHistory = true; +                        } +                        if (!isObject(state)) { +                            state = {}; +                            changeHistory = true; +                        } + +                        let {definitions} = content; +                        if (!Array.isArray(definitions)) { +                            definitions = await this._findDefinitions(isTerms, source, urlSearchParams); +                            if (this._setContentToken !== token) { return; } +                            content.definitions = definitions; +                            changeHistory = true; +                        } + +                        if (changeHistory) { +                            this._historyStateUpdate(state, content); +                        } + +                        asigned = true; +                        eventArgs.source = source; +                        eventArgs.content = content; +                        this.trigger('contentUpdating', eventArgs); +                        await this._setContentTermsOrKanji(token, isTerms, definitions, state); +                    } +                    break; +                case 'unloaded': +                    { +                        const {content} = this._history; +                        eventArgs.content = content; +                        this.trigger('contentUpdating', eventArgs); +                        this._setContentExtensionUnloaded(); +                    } +                    break; +            } + +            if (!asigned) { +                const {content} = this._history; +                eventArgs.type = 'clear'; +                eventArgs.content = content; +                this.trigger('contentUpdating', eventArgs); +                this._clearContent(); +            } + +            eventArgs.stale = (this._setContentToken !== token); +            this.trigger('contentUpdated', eventArgs); +        } catch (e) { +            this.onError(e); +        } finally { +            if (this._setContentToken === token) { +                this._setContentToken = null; +            } +        } +    } +      _onExtensionUnloaded() { -        this._setContentExtensionUnloaded(); +        this.setContent({ +            focus: false, +            history: false, +            params: {type: 'unloaded'}, +            state: {}, +            content: {} +        });      }      _onSourceTermView(e) { @@ -365,27 +449,32 @@ class Display {      async _onKanjiLookup(e) {          try {              e.preventDefault(); -            if (!this._context) { return; } +            if (!this._historyHasState()) { return; }              const link = e.target; -            this._context.update({ -                index: this._entryIndexFind(link), -                scroll: this._windowScroll.y -            }); -            const context = { -                sentence: this._context.get('sentence'), -                url: this._context.get('url') -            }; +            const {state} = this._history; + +            state.index = this._entryIndexFind(link); +            state.scroll = this._windowScroll.y; +            this._historyStateUpdate(state); -            const source = link.textContent; -            const definitions = await api.kanjiFind(source, this.getOptionsContext()); +            const query = link.textContent; +            const definitions = await api.kanjiFind(query, this.getOptionsContext());              this.setContent({                  focus: false,                  history: true, -                type: 'kanji', -                source, -                definitions, -                context +                params: { +                    type: 'kanji', +                    query, +                    wildcards: 'off' +                }, +                state: { +                    sentence: state.sentence, +                    url: state.url +                }, +                content: { +                    definitions +                }              });          } catch (error) {              this.onError(error); @@ -410,10 +499,12 @@ class Display {      async _onTermLookup(e) {          try { -            if (!this._context) { return; } +            if (!this._historyHasState()) { return; }              const termLookupResults = await this._termLookup(e); -            if (!termLookupResults) { return; } +            if (!termLookupResults || !this._historyHasState()) { return; } + +            const {state} = this._history;              const {textSource, definitions} = termLookupResults;              const scannedElement = e.target; @@ -421,22 +512,25 @@ class Display {              const layoutAwareScan = this._options.scanning.layoutAwareScan;              const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); -            this._context.update({ -                index: this._entryIndexFind(scannedElement), -                scroll: this._windowScroll.y -            }); -            const context = { -                sentence, -                url: this._context.get('url') -            }; +            state.index = this._entryIndexFind(scannedElement); +            state.scroll = this._windowScroll.y; +            this._historyStateUpdate(state);              this.setContent({                  focus: false,                  history: true, -                type: 'terms', -                source: textSource.text(), -                definitions, -                context +                params: { +                    type: 'terms', +                    query: textSource.text(), +                    wildcards: 'off' +                }, +                state: { +                    sentence, +                    url: state.url +                }, +                content: { +                    definitions +                }              });          } catch (error) {              this.onError(error); @@ -583,7 +677,7 @@ class Display {              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) { +            if (this._options !== null && this._options.scanning.enablePopupSearch) {                  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)); @@ -593,7 +687,34 @@ class Display {          }      } -    async _setContentTermsOrKanji(isTerms, definitions, sentence, url, index, scroll, token) { +    async _findDefinitions(isTerms, source, urlSearchParams) { +        const optionsContext = this.getOptionsContext(); +        if (isTerms) { +            const findDetails = {}; +            if (urlSearchParams.get('wildcards') !== 'off') { +                const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(source); +                if (match !== null) { +                    if (match[1]) { +                        findDetails.wildcard = 'prefix'; +                    } else if (match[3]) { +                        findDetails.wildcard = 'suffix'; +                    } +                    source = match[2]; +                } +            } + +            const {definitions} = await api.termsFind(source, findDetails, optionsContext); +            return definitions; +        } else { +            const definitions = await api.kanjiFind(source, optionsContext); +            return definitions; +        } +    } + +    async _setContentTermsOrKanji(token, isTerms, definitions, {sentence=null, url=null, index=0, scroll=null}) { +        if (typeof url !== 'string') { url = window.location.href; } +        sentence = this._getValidSentenceData(sentence); +          this._setEventListenersActive(false);          this._definitions = definitions; @@ -603,7 +724,7 @@ class Display {              definition.url = url;          } -        this._updateNavigation(this._context.previous, this._context.next); +        this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());          this._setNoContentVisible(definitions.length === 0);          const container = this._container; @@ -657,6 +778,11 @@ class Display {          this._setNoContentVisible(false);      } +    _clearContent() { +        this._setEventListenersActive(false); +        this._container.textContent = ''; +    } +      _setNoContentVisible(visible) {          const noResults = document.querySelector('#no-results'); @@ -746,24 +872,11 @@ class Display {      }      _relativeTermView(next) { -        if (this._context === null) { return false; } - -        const relative = next ? this._context.next : this._context.previous; -        if (!relative) { return false; } - -        this._context.update({ -            index: this._index, -            scroll: this._windowScroll.y -        }); -        this.setContent({ -            focus: false, -            history: false, -            type: relative.type, -            source: relative.source, -            definitions: relative.definitions, -            context: relative.context -        }); -        return true; +        if (next) { +            return this._history.hasNext() && this._history.forward(); +        } else { +            return this._history.hasPrevious() && this._history.back(); +        }      }      _noteTryAdd(mode) { @@ -913,6 +1026,13 @@ class Display {          return index >= 0 && index < entries.length ? entries[index] : null;      } +    _getValidSentenceData(sentence) { +        let {text, offset} = (isObject(sentence) ? sentence : {}); +        if (typeof text !== 'string') { text = ''; } +        if (typeof offset !== 'number') { offset = 0; } +        return {text, offset}; +    } +      _clozeBuild({text, offset}, source) {          return {              sentence: text.trim(), @@ -1000,4 +1120,20 @@ class Display {              this._audioPlay(this._definitions[index], this._getFirstExpressionIndex(), index);          }      } + +    _historyHasState() { +        return isObject(this._history.state); +    } + +    _historyStateUpdate(state, content) { +        const historyChangeIgnorePre = this._historyChangeIgnore; +        try { +            this._historyChangeIgnore = true; +            if (typeof state === 'undefined') { state = this._history.state; } +            if (typeof content === 'undefined') { content = this._history.content; } +            this._history.replaceState(state, content); +        } finally { +            this._historyChangeIgnore = historyChangeIgnorePre; +        } +    }  } |