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
This commit is contained in:
parent
e153971cd4
commit
208217198e
@ -33,6 +33,7 @@ class DisplaySearch extends Display {
|
|||||||
this._intro = document.querySelector('#intro');
|
this._intro = document.querySelector('#intro');
|
||||||
this._clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable');
|
this._clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable');
|
||||||
this._wanakanaEnable = document.querySelector('#wanakana-enable');
|
this._wanakanaEnable = document.querySelector('#wanakana-enable');
|
||||||
|
this._queryText = '';
|
||||||
this._introVisible = true;
|
this._introVisible = true;
|
||||||
this._introAnimationTimer = null;
|
this._introAnimationTimer = null;
|
||||||
this._clipboardMonitor = new ClipboardMonitor({
|
this._clipboardMonitor = new ClipboardMonitor({
|
||||||
@ -68,6 +69,9 @@ class DisplaySearch extends Display {
|
|||||||
await this._queryParser.prepare();
|
await this._queryParser.prepare();
|
||||||
|
|
||||||
this._queryParser.on('searched', this._onQueryParserSearch.bind(this));
|
this._queryParser.on('searched', this._onQueryParserSearch.bind(this));
|
||||||
|
this.on('contentUpdating', this._onContentUpdating.bind(this));
|
||||||
|
|
||||||
|
this.setHistorySettings({useBrowserHistory: true});
|
||||||
|
|
||||||
const options = this.getOptions();
|
const options = this.getOptions();
|
||||||
|
|
||||||
@ -83,7 +87,6 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._setQuery(query);
|
this._setQuery(query);
|
||||||
this._onSearchQueryUpdated(this._query.value, false);
|
|
||||||
|
|
||||||
if (mode !== 'popup') {
|
if (mode !== 'popup') {
|
||||||
if (options.general.enableClipboardMonitor === true) {
|
if (options.general.enableClipboardMonitor === true) {
|
||||||
@ -100,7 +103,6 @@ class DisplaySearch extends Display {
|
|||||||
this._search.addEventListener('click', this._onSearch.bind(this), false);
|
this._search.addEventListener('click', this._onSearch.bind(this), false);
|
||||||
this._query.addEventListener('input', this._onSearchInput.bind(this), false);
|
this._query.addEventListener('input', this._onSearchInput.bind(this), false);
|
||||||
this._wanakanaEnable.addEventListener('change', this._onWanakanaEnableChange.bind(this));
|
this._wanakanaEnable.addEventListener('change', this._onWanakanaEnableChange.bind(this));
|
||||||
window.addEventListener('popstate', this._onPopState.bind(this));
|
|
||||||
window.addEventListener('copy', this._onCopy.bind(this));
|
window.addEventListener('copy', this._onCopy.bind(this));
|
||||||
this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this));
|
this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this));
|
||||||
|
|
||||||
@ -108,6 +110,8 @@ class DisplaySearch extends Display {
|
|||||||
|
|
||||||
await this._prepareNestedPopups();
|
await this._prepareNestedPopups();
|
||||||
|
|
||||||
|
this.initializeState();
|
||||||
|
|
||||||
this._isPrepared = true;
|
this._isPrepared = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,29 +162,47 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setContent(...args) {
|
|
||||||
this._query.blur();
|
|
||||||
this._closePopups();
|
|
||||||
return await super.setContent(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearContent() {
|
|
||||||
this._closePopups();
|
|
||||||
return super.clearContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
|
_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();
|
||||||
|
this._setQuery(source);
|
||||||
|
this._setIntroVisible(!valid, animate);
|
||||||
|
this._setTitleText(source);
|
||||||
|
this._updateSearchButton();
|
||||||
|
}
|
||||||
|
|
||||||
_onQueryParserSearch({type, definitions, sentence, cause, textSource}) {
|
_onQueryParserSearch({type, definitions, sentence, cause, textSource}) {
|
||||||
this.setContent({
|
this.setContent({
|
||||||
focus: false,
|
focus: false,
|
||||||
history: cause !== 'mouse',
|
history: cause !== 'mouse',
|
||||||
|
params: {
|
||||||
type,
|
type,
|
||||||
source: textSource.text(),
|
query: textSource.text(),
|
||||||
definitions,
|
wildcards: 'off'
|
||||||
context: {
|
},
|
||||||
|
state: {
|
||||||
sentence,
|
sentence,
|
||||||
url: window.location.href
|
url: window.location.href
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
definitions
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -202,22 +224,9 @@ class DisplaySearch extends Display {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const query = this._query.value;
|
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);
|
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) {
|
_onRuntimeMessage({action, params}, sender, callback) {
|
||||||
const messageHandler = this._runtimeMessageHandlers.get(action);
|
const messageHandler = this._runtimeMessageHandlers.get(action);
|
||||||
if (typeof messageHandler === 'undefined') { return false; }
|
if (typeof messageHandler === 'undefined') { return false; }
|
||||||
@ -230,49 +239,25 @@ class DisplaySearch extends Display {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onExternalSearchUpdate({text, animate=true}) {
|
_onExternalSearchUpdate({text, animate=true}) {
|
||||||
this._setQuery(text);
|
this._onSearchQueryUpdated(text, animate);
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('query', text);
|
|
||||||
window.history.pushState(null, '', url.toString());
|
|
||||||
this._onSearchQueryUpdated(this._query.value, animate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onSearchQueryUpdated(query, animate) {
|
_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({
|
this.setContent({
|
||||||
focus: false,
|
focus: false,
|
||||||
history: false,
|
history: false,
|
||||||
definitions,
|
params: {
|
||||||
source: query,
|
query
|
||||||
type: 'terms',
|
},
|
||||||
context: {
|
state: {
|
||||||
sentence: {text: query, offset: 0},
|
sentence: {text: query, offset: 0},
|
||||||
url: window.location.href
|
url: window.location.href
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
definitions: null,
|
||||||
|
animate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.clearContent();
|
|
||||||
}
|
|
||||||
this._setTitleText(query);
|
|
||||||
} catch (e) {
|
|
||||||
this.onError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWanakanaEnableChange(e) {
|
_onWanakanaEnableChange(e) {
|
||||||
@ -335,6 +320,8 @@ class DisplaySearch extends Display {
|
|||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this._queryText === interpretedQuery) { return; }
|
||||||
|
this._queryText = interpretedQuery;
|
||||||
this._query.value = interpretedQuery;
|
this._query.value = interpretedQuery;
|
||||||
this._queryParser.setText(interpretedQuery);
|
this._queryParser.setText(interpretedQuery);
|
||||||
}
|
}
|
||||||
|
@ -83,9 +83,9 @@
|
|||||||
<script src="/fg/js/dom-text-scanner.js"></script>
|
<script src="/fg/js/dom-text-scanner.js"></script>
|
||||||
<script src="/fg/js/source.js"></script>
|
<script src="/fg/js/source.js"></script>
|
||||||
<script src="/mixed/js/audio-system.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.js"></script>
|
||||||
<script src="/mixed/js/display-generator.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/dynamic-loader.js"></script>
|
||||||
<script src="/mixed/js/media-loader.js"></script>
|
<script src="/mixed/js/media-loader.js"></script>
|
||||||
<script src="/mixed/js/scroll.js"></script>
|
<script src="/mixed/js/scroll.js"></script>
|
||||||
|
@ -50,9 +50,9 @@
|
|||||||
<script src="/fg/js/dom-text-scanner.js"></script>
|
<script src="/fg/js/dom-text-scanner.js"></script>
|
||||||
<script src="/fg/js/source.js"></script>
|
<script src="/fg/js/source.js"></script>
|
||||||
<script src="/mixed/js/audio-system.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.js"></script>
|
||||||
<script src="/mixed/js/display-generator.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/dynamic-loader.js"></script>
|
||||||
<script src="/mixed/js/frame-endpoint.js"></script>
|
<script src="/mixed/js/frame-endpoint.js"></script>
|
||||||
<script src="/mixed/js/media-loader.js"></script>
|
<script src="/mixed/js/media-loader.js"></script>
|
||||||
|
@ -50,6 +50,8 @@ class DisplayFloat extends Display {
|
|||||||
]);
|
]);
|
||||||
window.addEventListener('message', this._onWindowMessage.bind(this), false);
|
window.addEventListener('message', this._onWindowMessage.bind(this), false);
|
||||||
|
|
||||||
|
this.initializeState();
|
||||||
|
|
||||||
this._frameEndpoint.signal();
|
this._frameEndpoint.signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,12 +429,17 @@ class Frontend {
|
|||||||
{
|
{
|
||||||
focus,
|
focus,
|
||||||
history: false,
|
history: false,
|
||||||
|
params: {
|
||||||
type,
|
type,
|
||||||
source: textSource.text(),
|
query: textSource.text(),
|
||||||
definitions,
|
wildcards: 'off'
|
||||||
context: {
|
},
|
||||||
|
state: {
|
||||||
sentence,
|
sentence,
|
||||||
url
|
url
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
definitions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
178
ext/mixed/js/display-history.js
Normal file
178
ext/mixed/js/display-history.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -18,8 +18,8 @@
|
|||||||
/* global
|
/* global
|
||||||
* AudioSystem
|
* AudioSystem
|
||||||
* DOM
|
* DOM
|
||||||
* DisplayContext
|
|
||||||
* DisplayGenerator
|
* DisplayGenerator
|
||||||
|
* DisplayHistory
|
||||||
* Frontend
|
* Frontend
|
||||||
* MediaLoader
|
* MediaLoader
|
||||||
* PopupFactory
|
* PopupFactory
|
||||||
@ -30,14 +30,14 @@
|
|||||||
* dynamicLoader
|
* dynamicLoader
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Display {
|
class Display extends EventDispatcher {
|
||||||
constructor(spinner, container) {
|
constructor(spinner, container) {
|
||||||
|
super();
|
||||||
this._spinner = spinner;
|
this._spinner = spinner;
|
||||||
this._container = container;
|
this._container = container;
|
||||||
this._definitions = [];
|
this._definitions = [];
|
||||||
this._optionsContext = {depth: 0, url: window.location.href};
|
this._optionsContext = {depth: 0, url: window.location.href};
|
||||||
this._options = null;
|
this._options = null;
|
||||||
this._context = null;
|
|
||||||
this._index = 0;
|
this._index = 0;
|
||||||
this._audioPlaying = null;
|
this._audioPlaying = null;
|
||||||
this._audioFallback = null;
|
this._audioFallback = null;
|
||||||
@ -64,6 +64,9 @@ class Display {
|
|||||||
this._hotkeys = new Map();
|
this._hotkeys = new Map();
|
||||||
this._actions = new Map();
|
this._actions = new Map();
|
||||||
this._messageHandlers = new Map();
|
this._messageHandlers = new Map();
|
||||||
|
this._history = new DisplayHistory({clearable: true, useBrowserHistory: false});
|
||||||
|
this._historyChangeIgnore = false;
|
||||||
|
this._historyHasChanged = false;
|
||||||
|
|
||||||
this.registerActions([
|
this.registerActions([
|
||||||
['close', () => { this.onEscape(); }],
|
['close', () => { this.onEscape(); }],
|
||||||
@ -116,12 +119,27 @@ class Display {
|
|||||||
async prepare() {
|
async prepare() {
|
||||||
this._setInteractive(true);
|
this._setInteractive(true);
|
||||||
await this._displayGenerator.prepare();
|
await this._displayGenerator.prepare();
|
||||||
|
this._history.prepare();
|
||||||
|
this._history.on('stateChanged', this._onStateChanged.bind(this));
|
||||||
yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));
|
yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));
|
||||||
api.crossFrame.registerHandlers([
|
api.crossFrame.registerHandlers([
|
||||||
['popupMessage', {async: 'dynamic', handler: this._onMessage.bind(this)}]
|
['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) {
|
onError(error) {
|
||||||
if (yomichan.isExtensionUnloaded) { return; }
|
if (yomichan.isExtensionUnloaded) { return; }
|
||||||
yomichan.logError(error);
|
yomichan.logError(error);
|
||||||
@ -202,46 +220,25 @@ class Display {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setContent(details) {
|
setContent(details) {
|
||||||
const token = {}; // Unique identifier token
|
const {focus, history, params, state, content} = details;
|
||||||
this._setContentToken = token;
|
|
||||||
try {
|
|
||||||
this._mediaLoader.unloadAll();
|
|
||||||
|
|
||||||
const {focus, history, type, source, definitions, context} = details;
|
if (focus) {
|
||||||
|
|
||||||
if (!history) {
|
|
||||||
this._context = new DisplayContext(type, source, definitions, context);
|
|
||||||
} else {
|
|
||||||
this._context = DisplayContext.push(this._context, type, source, definitions, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focus !== false) {
|
|
||||||
window.focus();
|
window.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
const urlSearchParams = new URLSearchParams();
|
||||||
case 'terms':
|
for (const [key, value] of Object.entries(params)) {
|
||||||
case 'kanji':
|
urlSearchParams.append(key, value);
|
||||||
{
|
|
||||||
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 url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`;
|
||||||
|
|
||||||
clearContent() {
|
if (history && this._historyHasChanged) {
|
||||||
this._setEventListenersActive(false);
|
this._history.pushState(state, content, url);
|
||||||
this._container.textContent = '';
|
} else {
|
||||||
this._setEventListenersActive(true);
|
this._history.clear();
|
||||||
|
this._history.replaceState(state, content, url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCustomCss(css) {
|
setCustomCss(css) {
|
||||||
@ -348,9 +345,96 @@ class Display {
|
|||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
_onExtensionUnloaded() {
|
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();
|
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.setContent({
|
||||||
|
focus: false,
|
||||||
|
history: false,
|
||||||
|
params: {type: 'unloaded'},
|
||||||
|
state: {},
|
||||||
|
content: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_onSourceTermView(e) {
|
_onSourceTermView(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -365,27 +449,32 @@ class Display {
|
|||||||
async _onKanjiLookup(e) {
|
async _onKanjiLookup(e) {
|
||||||
try {
|
try {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this._context) { return; }
|
if (!this._historyHasState()) { return; }
|
||||||
|
|
||||||
const link = e.target;
|
const link = e.target;
|
||||||
this._context.update({
|
const {state} = this._history;
|
||||||
index: this._entryIndexFind(link),
|
|
||||||
scroll: this._windowScroll.y
|
|
||||||
});
|
|
||||||
const context = {
|
|
||||||
sentence: this._context.get('sentence'),
|
|
||||||
url: this._context.get('url')
|
|
||||||
};
|
|
||||||
|
|
||||||
const source = link.textContent;
|
state.index = this._entryIndexFind(link);
|
||||||
const definitions = await api.kanjiFind(source, this.getOptionsContext());
|
state.scroll = this._windowScroll.y;
|
||||||
|
this._historyStateUpdate(state);
|
||||||
|
|
||||||
|
const query = link.textContent;
|
||||||
|
const definitions = await api.kanjiFind(query, this.getOptionsContext());
|
||||||
this.setContent({
|
this.setContent({
|
||||||
focus: false,
|
focus: false,
|
||||||
history: true,
|
history: true,
|
||||||
|
params: {
|
||||||
type: 'kanji',
|
type: 'kanji',
|
||||||
source,
|
query,
|
||||||
definitions,
|
wildcards: 'off'
|
||||||
context
|
},
|
||||||
|
state: {
|
||||||
|
sentence: state.sentence,
|
||||||
|
url: state.url
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
definitions
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.onError(error);
|
this.onError(error);
|
||||||
@ -410,10 +499,12 @@ class Display {
|
|||||||
|
|
||||||
async _onTermLookup(e) {
|
async _onTermLookup(e) {
|
||||||
try {
|
try {
|
||||||
if (!this._context) { return; }
|
if (!this._historyHasState()) { return; }
|
||||||
|
|
||||||
const termLookupResults = await this._termLookup(e);
|
const termLookupResults = await this._termLookup(e);
|
||||||
if (!termLookupResults) { return; }
|
if (!termLookupResults || !this._historyHasState()) { return; }
|
||||||
|
|
||||||
|
const {state} = this._history;
|
||||||
const {textSource, definitions} = termLookupResults;
|
const {textSource, definitions} = termLookupResults;
|
||||||
|
|
||||||
const scannedElement = e.target;
|
const scannedElement = e.target;
|
||||||
@ -421,22 +512,25 @@ class Display {
|
|||||||
const layoutAwareScan = this._options.scanning.layoutAwareScan;
|
const layoutAwareScan = this._options.scanning.layoutAwareScan;
|
||||||
const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan);
|
const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan);
|
||||||
|
|
||||||
this._context.update({
|
state.index = this._entryIndexFind(scannedElement);
|
||||||
index: this._entryIndexFind(scannedElement),
|
state.scroll = this._windowScroll.y;
|
||||||
scroll: this._windowScroll.y
|
this._historyStateUpdate(state);
|
||||||
});
|
|
||||||
const context = {
|
|
||||||
sentence,
|
|
||||||
url: this._context.get('url')
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setContent({
|
this.setContent({
|
||||||
focus: false,
|
focus: false,
|
||||||
history: true,
|
history: true,
|
||||||
|
params: {
|
||||||
type: 'terms',
|
type: 'terms',
|
||||||
source: textSource.text(),
|
query: textSource.text(),
|
||||||
definitions,
|
wildcards: 'off'
|
||||||
context
|
},
|
||||||
|
state: {
|
||||||
|
sentence,
|
||||||
|
url: state.url
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
definitions
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.onError(error);
|
this.onError(error);
|
||||||
@ -583,7 +677,7 @@ class Display {
|
|||||||
this.addMultipleEventListeners('.action-view-note', 'click', this._onNoteView.bind(this));
|
this.addMultipleEventListeners('.action-view-note', 'click', this._onNoteView.bind(this));
|
||||||
this.addMultipleEventListeners('.action-play-audio', 'click', this._onAudioPlay.bind(this));
|
this.addMultipleEventListeners('.action-play-audio', 'click', this._onAudioPlay.bind(this));
|
||||||
this.addMultipleEventListeners('.kanji-link', 'click', this._onKanjiLookup.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', 'mouseup', this._onGlossaryMouseUp.bind(this));
|
||||||
this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this._onGlossaryMouseDown.bind(this));
|
this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this._onGlossaryMouseDown.bind(this));
|
||||||
this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousemove', this._onGlossaryMouseMove.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._setEventListenersActive(false);
|
||||||
|
|
||||||
this._definitions = definitions;
|
this._definitions = definitions;
|
||||||
@ -603,7 +724,7 @@ class Display {
|
|||||||
definition.url = url;
|
definition.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateNavigation(this._context.previous, this._context.next);
|
this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
|
||||||
this._setNoContentVisible(definitions.length === 0);
|
this._setNoContentVisible(definitions.length === 0);
|
||||||
|
|
||||||
const container = this._container;
|
const container = this._container;
|
||||||
@ -657,6 +778,11 @@ class Display {
|
|||||||
this._setNoContentVisible(false);
|
this._setNoContentVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clearContent() {
|
||||||
|
this._setEventListenersActive(false);
|
||||||
|
this._container.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
_setNoContentVisible(visible) {
|
_setNoContentVisible(visible) {
|
||||||
const noResults = document.querySelector('#no-results');
|
const noResults = document.querySelector('#no-results');
|
||||||
|
|
||||||
@ -746,24 +872,11 @@ class Display {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_relativeTermView(next) {
|
_relativeTermView(next) {
|
||||||
if (this._context === null) { return false; }
|
if (next) {
|
||||||
|
return this._history.hasNext() && this._history.forward();
|
||||||
const relative = next ? this._context.next : this._context.previous;
|
} else {
|
||||||
if (!relative) { return false; }
|
return this._history.hasPrevious() && this._history.back();
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_noteTryAdd(mode) {
|
_noteTryAdd(mode) {
|
||||||
@ -913,6 +1026,13 @@ class Display {
|
|||||||
return index >= 0 && index < entries.length ? entries[index] : null;
|
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) {
|
_clozeBuild({text, offset}, source) {
|
||||||
return {
|
return {
|
||||||
sentence: text.trim(),
|
sentence: text.trim(),
|
||||||
@ -1000,4 +1120,20 @@ class Display {
|
|||||||
this._audioPlay(this._definitions[index], this._getFirstExpressionIndex(), index);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user