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._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);
|
||||
}
|
||||
|
||||
clearContent() {
|
||||
this._closePopups();
|
||||
return super.clearContent();
|
||||
}
|
||||
|
||||
// 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}) {
|
||||
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];
|
||||
_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
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -50,6 +50,8 @@ class DisplayFloat extends Display {
|
||||
]);
|
||||
window.addEventListener('message', this._onWindowMessage.bind(this), false);
|
||||
|
||||
this.initializeState();
|
||||
|
||||
this._frameEndpoint.signal();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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
|
||||
* 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();
|
||||
setContent(details) {
|
||||
const {focus, history, params, state, content} = details;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (focus !== false) {
|
||||
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;
|
||||
}
|
||||
if (focus) {
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
|
||||
clearContent() {
|
||||
this._setEventListenersActive(false);
|
||||
this._container.textContent = '';
|
||||
this._setEventListenersActive(true);
|
||||
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()}`;
|
||||
|
||||
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;
|
||||
|
||||
const source = link.textContent;
|
||||
const definitions = await api.kanjiFind(source, this.getOptionsContext());
|
||||
state.index = this._entryIndexFind(link);
|
||||
state.scroll = this._windowScroll.y;
|
||||
this._historyStateUpdate(state);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user