aboutsummaryrefslogtreecommitdiff
path: root/ext/mixed/js/display.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-07-26 16:51:54 -0400
committerGitHub <noreply@github.com>2020-07-26 16:51:54 -0400
commit208217198e9228699e7299f06d3701899d44d8bb (patch)
treeaae11d6af70bac0c61774b8f611d9117101a288f /ext/mixed/js/display.js
parente153971cd4a5768a6c7dc9df36cf446cf298227d (diff)
Display history refactor (#691)
* Create DisplayHistory * Change arguments for _setContentTermsOrKanji * Set up history-driven content updates * Use new history only * Load definitions if missing * Refactor definitions getting * Add support for wildcards * Move definitions setup * Add events * Allow state change even if there is no history state * Update search page to use history * Fix history overwriting * Fix search page not seeing state chang events during prepare * Update state if necessary * Don't reassign query text if the same * Remove DisplayContext * Initialize with real history state * Track URL * Update DisplayHistory to support pseudo-history * Configure history settings on search page * Fix state * Use full URL * Change data format of setContent * Rename details to content * Update event arguments * Fix animation * Remove old state changes * Clear content properly * Remove set/clear content overrides * Fix setting up event listeners for content clear * Make clearContent private * Make focus opt-in * Validate source * Add unloaded type * Generalize content params * Update how extension unload content is assigned * Restore query blurring
Diffstat (limited to 'ext/mixed/js/display.js')
-rw-r--r--ext/mixed/js/display.js316
1 files changed, 226 insertions, 90 deletions
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;
+ }
+ }
}