summaryrefslogtreecommitdiff
path: root/ext/mixed/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mixed/js')
-rw-r--r--ext/mixed/js/audio.js4
-rw-r--r--ext/mixed/js/core.js (renamed from ext/mixed/js/extension.js)85
-rw-r--r--ext/mixed/js/display-context.js55
-rw-r--r--ext/mixed/js/display.js163
-rw-r--r--ext/mixed/js/dom.js66
-rw-r--r--ext/mixed/js/japanese.js22
6 files changed, 310 insertions, 85 deletions
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
index 4e9d04fa..35f283a4 100644
--- a/ext/mixed/js/audio.js
+++ b/ext/mixed/js/audio.js
@@ -68,7 +68,7 @@ class TextToSpeechAudio {
}
static createFromUri(ttsUri) {
- const m = /^tts:[^#\?]*\?([^#]*)/.exec(ttsUri);
+ const m = /^tts:[^#?]*\?([^#]*)/.exec(ttsUri);
if (m === null) { return null; }
const searchParameters = {};
@@ -113,7 +113,7 @@ function audioGetFromUrl(url, willDownload) {
async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) {
const key = `${expression.expression}:${expression.reading}`;
- if (cache !== null && cache.hasOwnProperty(expression)) {
+ if (cache !== null && hasOwn(cache, expression)) {
return cache[key];
}
diff --git a/ext/mixed/js/extension.js b/ext/mixed/js/core.js
index 12ed9c1f..b5911535 100644
--- a/ext/mixed/js/extension.js
+++ b/ext/mixed/js/core.js
@@ -17,27 +17,11 @@
*/
-// toIterable is required on Edge for cross-window origin objects.
-function toIterable(value) {
- if (typeof Symbol !== 'undefined' && typeof value[Symbol.iterator] !== 'undefined') {
- return value;
- }
-
- if (value !== null && typeof value === 'object') {
- const length = value.length;
- if (typeof length === 'number' && Number.isFinite(length)) {
- const array = [];
- for (let i = 0; i < length; ++i) {
- array.push(value[i]);
- }
- return array;
- }
- }
-
- throw new Error('Could not convert to iterable');
-}
+/*
+ * Extension information
+ */
-function extensionHasChrome() {
+function _extensionHasChrome() {
try {
return typeof chrome === 'object' && chrome !== null;
} catch (e) {
@@ -45,7 +29,7 @@ function extensionHasChrome() {
}
}
-function extensionHasBrowser() {
+function _extensionHasBrowser() {
try {
return typeof browser === 'object' && browser !== null;
} catch (e) {
@@ -53,6 +37,21 @@ function extensionHasBrowser() {
}
}
+const EXTENSION_IS_BROWSER_EDGE = (
+ _extensionHasBrowser() &&
+ (!_extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined'))
+);
+
+if (EXTENSION_IS_BROWSER_EDGE) {
+ // Edge does not have chrome defined.
+ chrome = browser;
+}
+
+
+/*
+ * Error handling
+ */
+
function errorToJson(error) {
return {
name: error.name,
@@ -86,16 +85,44 @@ function logError(error, alert) {
}
}
-const EXTENSION_IS_BROWSER_EDGE = (
- extensionHasBrowser() &&
- (!extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined'))
-);
-if (EXTENSION_IS_BROWSER_EDGE) {
- // Edge does not have chrome defined.
- chrome = browser;
+/*
+ * Common helpers
+ */
+
+function isObject(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function hasOwn(object, property) {
+ return Object.prototype.hasOwnProperty.call(object, property);
}
+// toIterable is required on Edge for cross-window origin objects.
+function toIterable(value) {
+ if (typeof Symbol !== 'undefined' && typeof value[Symbol.iterator] !== 'undefined') {
+ return value;
+ }
+
+ if (value !== null && typeof value === 'object') {
+ const length = value.length;
+ if (typeof length === 'number' && Number.isFinite(length)) {
+ const array = [];
+ for (let i = 0; i < length; ++i) {
+ array.push(value[i]);
+ }
+ return array;
+ }
+ }
+
+ throw new Error('Could not convert to iterable');
+}
+
+
+/*
+ * Async utilities
+ */
+
function promiseTimeout(delay, resolveValue) {
if (delay <= 0) {
return Promise.resolve(resolveValue);
@@ -146,5 +173,5 @@ function stringReplaceAsync(str, regex, replacer) {
return Promise.resolve(str);
}
parts.push(str.substring(index));
- return Promise.all(parts).then(v => v.join(''));
+ return Promise.all(parts).then((v) => v.join(''));
}
diff --git a/ext/mixed/js/display-context.js b/ext/mixed/js/display-context.js
new file mode 100644
index 00000000..4b399881
--- /dev/null
+++ b/ext/mixed/js/display-context.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+class DisplayContext {
+ constructor(type, definitions, context) {
+ this.type = type;
+ 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, definitions, context) {
+ const newContext = new DisplayContext(type, definitions, context);
+ if (self !== null) {
+ newContext.update({previous: self});
+ self.update({next: newContext});
+ }
+ return newContext;
+ }
+}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index f231fab5..c32852ad 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -42,7 +42,7 @@ class Display {
this.setInteractive(true);
}
- onError(error) {
+ onError(_error) {
throw new Error('Override me');
}
@@ -55,26 +55,24 @@ class Display {
this.sourceTermView();
}
+ onNextTermView(e) {
+ e.preventDefault();
+ this.nextTermView();
+ }
+
async onKanjiLookup(e) {
try {
e.preventDefault();
if (!this.context) { return; }
const link = e.target;
- this.windowScroll.toY(0);
+ this.context.update({
+ index: this.entryIndexFind(link),
+ scroll: this.windowScroll.y
+ });
const context = {
- source: {
- type: 'terms',
- details: {
- definitions: this.definitions,
- context: Object.assign({}, this.context, {
- index: this.entryIndexFind(link),
- scroll: this.windowScroll.y
- })
- }
- },
- sentence: this.context.sentence,
- url: this.context.url
+ sentence: this.context.get('sentence'),
+ url: this.context.get('url')
};
const definitions = await apiKanjiFind(link.textContent, this.getOptionsContext());
@@ -85,17 +83,17 @@ class Display {
}
onGlossaryMouseDown(e) {
- if (Frontend.isMouseButtonPressed('primary', e)) {
+ if (DOM.isMouseButtonPressed(e, 'primary')) {
this.clickScanPrevent = false;
}
}
- onGlossaryMouseMove(e) {
+ onGlossaryMouseMove() {
this.clickScanPrevent = true;
}
onGlossaryMouseUp(e) {
- if (!this.clickScanPrevent && Frontend.isMouseButtonPressed('primary', e)) {
+ if (!this.clickScanPrevent && DOM.isMouseButtonPressed(e, 'primary')) {
this.onTermLookup(e);
}
}
@@ -103,6 +101,7 @@ class Display {
async onTermLookup(e, {disableScroll, selectText, disableHistory}={}) {
try {
if (!this.context) { return; }
+
const termLookupResults = await this.termLookup(e);
if (!termLookupResults) { return; }
const {textSource, definitions} = termLookupResults;
@@ -110,25 +109,26 @@ class Display {
const scannedElement = e.target;
const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
- if (!disableScroll) {
- this.windowScroll.toY(0);
- }
-
+ this.context.update({
+ index: this.entryIndexFind(scannedElement),
+ scroll: this.windowScroll.y
+ });
const context = {
- source: disableHistory ? this.context.source : {
- type: 'terms',
- details: {
- definitions: this.definitions,
- context: Object.assign({}, this.context, {
- index: this.entryIndexFind(scannedElement),
- scroll: this.windowScroll.y
- })
- }
- },
disableScroll,
+ disableHistory,
sentence,
- url: this.context.url
+ url: this.context.get('url')
};
+ if (disableHistory) {
+ Object.assign(context, {
+ previous: this.context.previous,
+ next: this.context.next
+ });
+ } else {
+ Object.assign(context, {
+ previous: this.context
+ });
+ }
this.setContentTerms(definitions, context);
@@ -194,7 +194,7 @@ class Display {
onKeyDown(e) {
const key = Display.getKeyFromEvent(e);
const handlers = Display.onKeyDownHandlers;
- if (handlers.hasOwnProperty(key)) {
+ if (hasOwn(handlers, key)) {
const handler = handlers[key];
if (handler(this, e)) {
e.preventDefault();
@@ -206,9 +206,17 @@ class Display {
onWheel(e) {
if (e.altKey) {
- const delta = e.deltaY;
- if (delta !== 0) {
- this.entryScrollIntoView(this.index + (delta > 0 ? 1 : -1), null, true);
+ if (e.deltaY !== 0) {
+ this.entryScrollIntoView(this.index + (e.deltaY > 0 ? 1 : -1), null, true);
+ e.preventDefault();
+ }
+ } else if (e.shiftKey) {
+ const delta = -e.deltaX || e.deltaY;
+ if (delta > 0) {
+ this.sourceTermView();
+ e.preventDefault();
+ } else if (delta < 0) {
+ this.nextTermView();
e.preventDefault();
}
}
@@ -216,7 +224,7 @@ class Display {
onRuntimeMessage({action, params}, sender, callback) {
const handlers = Display.runtimeMessageHandlers;
- if (handlers.hasOwnProperty(action)) {
+ if (hasOwn(handlers, action)) {
const handler = handlers[action];
const result = handler(this, params);
callback(result);
@@ -292,6 +300,7 @@ class Display {
this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
+ this.addEventListeners('.next-term', 'click', this.onNextTermView.bind(this));
if (this.options.scanning.enablePopupSearch) {
this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this));
this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this));
@@ -335,12 +344,18 @@ class Display {
}
this.definitions = definitions;
- this.context = context;
+ if (context.disableHistory) {
+ delete context.disableHistory;
+ this.context = new DisplayContext('terms', definitions, context);
+ } else {
+ this.context = DisplayContext.push(this.context, 'terms', definitions, context);
+ }
const sequence = ++this.sequence;
const params = {
definitions,
- source: context.source,
+ source: this.context.previous,
+ next: this.context.next,
addable: options.anki.enable,
grouped: options.general.resultOutputMode === 'group',
merged: options.general.resultOutputMode === 'merge',
@@ -359,6 +374,9 @@ class Display {
const {index, scroll, disableScroll} = context;
if (!disableScroll) {
this.entryScrollIntoView(index || 0, scroll);
+ } else {
+ delete context.disableScroll;
+ this.entrySetCurrent(index || 0);
}
if (options.audio.enabled && options.audio.autoPlay) {
@@ -387,12 +405,18 @@ class Display {
}
this.definitions = definitions;
- this.context = context;
+ if (context.disableHistory) {
+ delete context.disableHistory;
+ this.context = new DisplayContext('kanji', definitions, context);
+ } else {
+ this.context = DisplayContext.push(this.context, 'kanji', definitions, context);
+ }
const sequence = ++this.sequence;
const params = {
definitions,
- source: context.source,
+ source: this.context.previous,
+ next: this.context.next,
addable: options.anki.enable,
debug: options.general.debugInfo
};
@@ -464,7 +488,7 @@ class Display {
}
}
- entryScrollIntoView(index, scroll, smooth) {
+ entrySetCurrent(index) {
index = Math.min(index, this.definitions.length - 1);
index = Math.max(index, 0);
@@ -478,13 +502,20 @@ class Display {
entry.classList.add('entry-current');
}
+ this.index = index;
+
+ return entry;
+ }
+
+ entryScrollIntoView(index, scroll, smooth) {
this.windowScroll.stop();
- let target;
- if (scroll) {
+ const entry = this.entrySetCurrent(index);
+ let target;
+ if (scroll !== null) {
target = scroll;
} else {
- target = index === 0 || entry === null ? 0 : Display.getElementTop(entry);
+ target = this.index === 0 || entry === null ? 0 : Display.getElementTop(entry);
}
if (smooth) {
@@ -492,14 +523,36 @@ class Display {
} else {
this.windowScroll.toY(target);
}
-
- this.index = index;
}
sourceTermView() {
- if (!this.context || !this.context.source) { return; }
- const {type, details} = this.context.source;
- this.setContent(type, details);
+ if (!this.context || !this.context.previous) { return; }
+ this.context.update({
+ index: this.index,
+ scroll: this.windowScroll.y
+ });
+ const previousContext = this.context.previous;
+ previousContext.set('disableHistory', true);
+ const details = {
+ definitions: previousContext.definitions,
+ context: previousContext.context
+ };
+ this.setContent(previousContext.type, details);
+ }
+
+ nextTermView() {
+ if (!this.context || !this.context.next) { return; }
+ this.context.update({
+ index: this.index,
+ scroll: this.windowScroll.y
+ });
+ const nextContext = this.context.next;
+ nextContext.set('disableHistory', true);
+ const details = {
+ definitions: nextContext.definitions,
+ context: nextContext.context
+ };
+ this.setContent(nextContext.type, details);
}
noteTryAdd(mode) {
@@ -574,7 +627,7 @@ class Display {
if (button !== null) {
let titleDefault = button.dataset.titleDefault;
if (!titleDefault) {
- titleDefault = button.title || "";
+ titleDefault = button.title || '';
button.dataset.titleDefault = titleDefault;
}
button.title = `${titleDefault}\n${info}`;
@@ -770,6 +823,14 @@ Display.onKeyDownHandlers = {
return false;
},
+ 'F': (self, e) => {
+ if (e.altKey) {
+ self.nextTermView();
+ return true;
+ }
+ return false;
+ },
+
'E': (self, e) => {
if (e.altKey) {
self.noteTryAdd('term-kanji');
diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js
new file mode 100644
index 00000000..4e4d49e3
--- /dev/null
+++ b/ext/mixed/js/dom.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+class DOM {
+ static isPointInRect(x, y, rect) {
+ return (
+ x >= rect.left && x < rect.right &&
+ y >= rect.top && y < rect.bottom
+ );
+ }
+
+ static isPointInAnyRect(x, y, rects) {
+ for (const rect of rects) {
+ if (DOM.isPointInRect(x, y, rect)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static isPointInSelection(x, y, selection) {
+ for (let i = 0; i < selection.rangeCount; ++i) {
+ const range = selection.getRangeAt(i);
+ if (DOM.isPointInAnyRect(x, y, range.getClientRects())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static isMouseButtonPressed(mouseEvent, button) {
+ const mouseEventButton = mouseEvent.button;
+ switch (button) {
+ case 'primary': return mouseEventButton === 0;
+ case 'secondary': return mouseEventButton === 2;
+ case 'auxiliary': return mouseEventButton === 1;
+ default: return false;
+ }
+ }
+
+ static isMouseButtonDown(mouseEvent, button) {
+ const mouseEventButtons = mouseEvent.buttons;
+ switch (button) {
+ case 'primary': return (mouseEventButtons & 0x1) !== 0x0;
+ case 'secondary': return (mouseEventButtons & 0x2) !== 0x0;
+ case 'auxiliary': return (mouseEventButtons & 0x4) !== 0x0;
+ default: return false;
+ }
+ }
+}
diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js
index a7cd0452..8b841b2e 100644
--- a/ext/mixed/js/japanese.js
+++ b/ext/mixed/js/japanese.js
@@ -91,8 +91,9 @@ function jpDistributeFurigana(expression, reading) {
return fallback;
}
+ let isAmbiguous = false;
const segmentize = (reading, groups) => {
- if (groups.length === 0) {
+ if (groups.length === 0 || isAmbiguous) {
return [];
}
@@ -106,14 +107,25 @@ function jpDistributeFurigana(expression, reading) {
}
}
} else {
+ let foundSegments = null;
for (let i = reading.length; i >= group.text.length; --i) {
const readingUsed = reading.substring(0, i);
const readingLeft = reading.substring(i);
const segs = segmentize(readingLeft, groups.slice(1));
if (segs) {
- return [{text: group.text, furigana: readingUsed}].concat(segs);
+ if (foundSegments !== null) {
+ // more than one way to segmentize the tail, mark as ambiguous
+ isAmbiguous = true;
+ return null;
+ }
+ foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs);
+ }
+ // there is only one way to segmentize the last non-kana group
+ if (groups.length === 1) {
+ break;
}
}
+ return foundSegments;
}
};
@@ -129,7 +141,11 @@ function jpDistributeFurigana(expression, reading) {
}
}
- return segmentize(reading, groups) || fallback;
+ const segments = segmentize(reading, groups);
+ if (segments && !isAmbiguous) {
+ return segments;
+ }
+ return fallback;
}
function jpDistributeFuriganaInflected(expression, reading, source) {