summaryrefslogtreecommitdiff
path: root/ext/fg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg')
-rw-r--r--ext/fg/css/client.css17
-rw-r--r--ext/fg/float.html12
-rw-r--r--ext/fg/js/api.js12
-rw-r--r--ext/fg/js/document.js10
-rw-r--r--ext/fg/js/float.js51
-rw-r--r--ext/fg/js/frontend-initialize.js20
-rw-r--r--ext/fg/js/frontend.js61
-rw-r--r--ext/fg/js/popup-nested.js5
-rw-r--r--ext/fg/js/popup-proxy-host.js44
-rw-r--r--ext/fg/js/popup-proxy.js25
-rw-r--r--ext/fg/js/popup.js144
-rw-r--r--ext/fg/js/source.js26
12 files changed, 281 insertions, 146 deletions
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css
index a2b06d0f..633c88ef 100644
--- a/ext/fg/css/client.css
+++ b/ext/fg/css/client.css
@@ -17,11 +17,11 @@
*/
-iframe#yomichan-float {
+iframe.yomichan-float {
all: initial;
background-color: #fff;
border: 1px solid #999;
- box-shadow: 0 0 10px rgba(0, 0, 0, .5);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
position: fixed;
resize: both;
visibility: hidden;
@@ -29,7 +29,14 @@ iframe#yomichan-float {
box-sizing: border-box;
}
-iframe#yomichan-float.yomichan-float-full-width {
+iframe.yomichan-float[data-yomichan-theme=dark],
+iframe.yomichan-float[data-yomichan-theme=auto][data-yomichan-site-color=dark] {
+ background-color: #1e1e1e;
+ border: 1px solid #666;
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
+}
+
+iframe.yomichan-float.yomichan-float-full-width {
border-left: none;
border-right: none;
left: 0 !important;
@@ -39,13 +46,13 @@ iframe#yomichan-float.yomichan-float-full-width {
resize: none;
}
-iframe#yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) {
+iframe.yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) {
border-bottom: none;
top: auto !important;
bottom: 0 !important;
}
-iframe#yomichan-float.yomichan-float-full-width.yomichan-float-above {
+iframe.yomichan-float.yomichan-float-full-width.yomichan-float-above {
border-top: none;
top: 0 !important;
bottom: auto !important;
diff --git a/ext/fg/float.html b/ext/fg/float.html
index fe1aee8f..580a7963 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -1,18 +1,12 @@
<!DOCTYPE html>
-<html lang="en">
+<html lang="en" data-yomichan-page="float">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title></title>
- <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
- <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/mixed/css/display.css">
- <style type="text/css">
- .entry, .note {
- padding-left: 10px;
- padding-right: 10px;
- }
- </style>
+ <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default">
+ <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark">
</head>
<body>
<div id="spinner">
diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js
index a553e514..b0746b85 100644
--- a/ext/fg/js/api.js
+++ b/ext/fg/js/api.js
@@ -49,8 +49,8 @@ function apiAudioGetUrl(definition, source, optionsContext) {
return utilInvoke('audioGetUrl', {definition, source, optionsContext});
}
-function apiCommandExec(command) {
- return utilInvoke('commandExec', {command});
+function apiCommandExec(command, params) {
+ return utilInvoke('commandExec', {command, params});
}
function apiScreenshotGet(options) {
@@ -64,3 +64,11 @@ function apiForward(action, params) {
function apiFrameInformationGet() {
return utilInvoke('frameInformationGet');
}
+
+function apiInjectStylesheet(css) {
+ return utilInvoke('injectStylesheet', {css});
+}
+
+function apiGetEnvironmentInfo() {
+ return utilInvoke('getEnvironmentInfo');
+}
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 94a68e6c..a168705e 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -27,8 +27,8 @@ function docImposterCreate(element, isTextarea) {
const elementStyle = window.getComputedStyle(element);
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
- const left = elementRect.left - documentRect.left;
- const top = elementRect.top - documentRect.top;
+ let left = elementRect.left - documentRect.left;
+ let top = elementRect.top - documentRect.top;
// Container
const container = document.createElement('div');
@@ -82,6 +82,12 @@ function docImposterCreate(element, isTextarea) {
docSetImposterStyle(imposterStyle, 'width', `${width}px`);
docSetImposterStyle(imposterStyle, 'height', `${height}px`);
}
+ if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) {
+ left += (elementRect.left - imposterRect.left);
+ top += (elementRect.top - imposterRect.top);
+ docSetImposterStyle(imposterStyle, 'left', `${left}px`);
+ docSetImposterStyle(imposterStyle, 'top', `${top}px`);
+ }
imposter.scrollTop = element.scrollTop;
imposter.scrollLeft = element.scrollLeft;
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 8fdb6925..089c9422 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -21,39 +21,23 @@ class DisplayFloat extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#definitions'));
this.autoPlayAudioTimer = null;
- this.styleNode = null;
this.optionsContext = {
depth: 0,
url: window.location.href
};
- this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
-
window.addEventListener('message', (e) => this.onMessage(e), false);
}
onError(error) {
if (window.yomichan_orphaned) {
- this.onOrphaned();
+ this.setContentOrphaned();
} else {
logError(error, true);
}
}
- onOrphaned() {
- const definitions = document.querySelector('#definitions');
- const errorOrphaned = document.querySelector('#error-orphaned');
-
- if (definitions !== null) {
- definitions.style.setProperty('display', 'none', 'important');
- }
-
- if (errorOrphaned !== null) {
- errorOrphaned.style.setProperty('display', 'block', 'important');
- }
- }
-
onSearchClear() {
window.parent.postMessage('popupClose', '*');
}
@@ -84,6 +68,10 @@ class DisplayFloat extends Display {
super.onKeyDown(e);
}
+ getOptionsContext() {
+ return this.optionsContext;
+ }
+
autoPlayAudio() {
this.clearAutoPlayTimer();
this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400);
@@ -96,29 +84,15 @@ class DisplayFloat extends Display {
}
}
- initialize(options, popupInfo, url) {
- const css = options.general.customPopupCss;
- if (css) {
- this.setStyle(css);
- }
+ async initialize(options, popupInfo, url, childrenSupported) {
+ await super.initialize(options);
const {id, depth, parentFrameId} = popupInfo;
this.optionsContext.depth = depth;
this.optionsContext.url = url;
- popupNestedInitialize(id, depth, parentFrameId, url);
- }
-
- setStyle(css) {
- const parent = document.head;
-
- if (this.styleNode === null) {
- this.styleNode = document.createElement('style');
- }
-
- this.styleNode.textContent = css;
- if (this.styleNode.parentNode !== parent) {
- parent.appendChild(this.styleNode);
+ if (childrenSupported) {
+ popupNestedInitialize(id, depth, parentFrameId, url);
}
}
}
@@ -134,11 +108,10 @@ DisplayFloat.onKeyDownHandlers = {
};
DisplayFloat.messageHandlers = {
- termsShow: (self, {definitions, options, context}) => self.termsShow(definitions, options, context),
- kanjiShow: (self, {definitions, options, context}) => self.kanjiShow(definitions, options, context),
+ setContent: (self, {type, details}) => self.setContent(type, details),
clearAutoPlayTimer: (self) => self.clearAutoPlayTimer(),
- orphaned: (self) => self.onOrphaned(),
- initialize: (self, {options, popupInfo, url}) => self.initialize(options, popupInfo, url)
+ setCustomCss: (self, {css}) => self.setCustomCss(css),
+ initialize: (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported)
};
window.yomichan_display = new DisplayFloat();
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
new file mode 100644
index 00000000..37a82faa
--- /dev/null
+++ b/ext/fg/js/frontend-initialize.js
@@ -0,0 +1,20 @@
+/*
+ * 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/>.
+ */
+
+
+window.yomichan_frontend = Frontend.create();
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 88cb93a9..e854f74e 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -41,14 +41,18 @@ class Frontend {
this.enabled = false;
this.eventListeners = [];
+
+ this.isPreparedPromiseResolve = null;
+ this.isPreparedPromise = new Promise((resolve) => { this.isPreparedPromiseResolve = resolve; });
+
+ this.lastShowPromise = Promise.resolve();
}
static create() {
- const initializationData = window.frontendInitializationData;
- const isNested = (initializationData !== null && typeof initializationData === 'object');
- const {id, depth, parentFrameId, ignoreNodes, url} = isNested ? initializationData : {};
+ const data = window.frontendInitializationData || {};
+ const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
- const popup = isNested ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null);
+ const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null, depth);
const frontend = new Frontend(popup, ignoreNodes);
frontend.prepare();
return frontend;
@@ -59,11 +63,16 @@ class Frontend {
await this.updateOptions();
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+ this.isPreparedPromiseResolve();
} catch (e) {
this.onError(e);
}
}
+ isPrepared() {
+ return this.isPreparedPromise;
+ }
+
onMouseOver(e) {
if (e.target === this.popup.container && this.popupTimer !== null) {
this.popupTimerClear();
@@ -130,8 +139,14 @@ class Frontend {
}
}
- onResize() {
- this.searchClear(false);
+ async onResize() {
+ if (this.textSourceLast !== null && await this.popup.isVisibleAsync()) {
+ const textSource = this.textSourceLast;
+ this.lastShowPromise = this.popup.showContent(
+ textSource.getRect(),
+ textSource.getWritingMode()
+ );
+ }
}
onClick(e) {
@@ -222,8 +237,8 @@ class Frontend {
const handlers = Frontend.runtimeMessageHandlers;
if (handlers.hasOwnProperty(action)) {
const handler = handlers[action];
- handler(this, params);
- callback();
+ const result = handler(this, params);
+ callback(result);
}
}
@@ -279,6 +294,7 @@ class Frontend {
async updateOptions() {
this.options = await apiOptionsGet(this.getOptionsContext());
this.setEnabled(this.options.general.enable);
+ await this.popup.setOptions(this.options);
}
popupTimerSet(callback) {
@@ -303,6 +319,10 @@ class Frontend {
}
const textSource = docRangeFromPoint(x, y, this.options);
+ return await this.searchSource(textSource, cause);
+ }
+
+ async searchSource(textSource, cause) {
let hideResults = textSource === null;
let searched = false;
let success = false;
@@ -318,10 +338,10 @@ class Frontend {
} catch (e) {
if (window.yomichan_orphaned) {
if (textSource && this.options.scanning.modifier !== 'none') {
- this.popup.showOrphaned(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- this.options
+ 'orphaned'
);
}
} else {
@@ -357,12 +377,11 @@ class Frontend {
const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
const url = window.location.href;
- this.popup.termsShow(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- definitions,
- this.options,
- {sentence, url, focus}
+ 'terms',
+ {definitions, context: {sentence, url, focus}}
);
this.textSourceLast = textSource;
@@ -388,12 +407,11 @@ class Frontend {
const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
const url = window.location.href;
- this.popup.kanjiShow(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- definitions,
- this.options,
- {sentence, url, focus}
+ 'kanji',
+ {definitions, context: {sentence, url, focus}}
);
this.textSourceLast = textSource;
@@ -558,8 +576,9 @@ Frontend.runtimeMessageHandlers = {
popupSetVisibleOverride: (self, {visible}) => {
self.popup.setVisibleOverride(visible);
+ },
+
+ getUrl: () => {
+ return {url: window.location.href};
}
};
-
-
-window.yomichan_frontend = Frontend.create();
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index b36de2ec..cec95aea 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -35,13 +35,14 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ];
- window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url};
+ window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true};
const scriptSrcs = [
'/fg/js/frontend-api-sender.js',
'/fg/js/popup.js',
'/fg/js/popup-proxy.js',
- '/fg/js/frontend.js'
+ '/fg/js/frontend.js',
+ '/fg/js/frontend-initialize.js'
];
for (const src of scriptSrcs) {
const script = document.createElement('script');
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index f933639c..d8dec4df 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -38,21 +38,23 @@ class PopupProxyHost {
this.apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, {
createNestedPopup: ({parentId}) => this.createNestedPopup(parentId),
- show: ({id, elementRect, options}) => this.show(id, elementRect, options),
- showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options),
+ setOptions: ({id, options}) => this.setOptions(id, options),
hide: ({id, changeFocus}) => this.hide(id, changeFocus),
+ isVisibleAsync: ({id}) => this.isVisibleAsync(id),
setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible),
containsPoint: ({id, x, y}) => this.containsPoint(id, x, y),
- termsShow: ({id, elementRect, writingMode, definitions, options, context}) => this.termsShow(id, elementRect, writingMode, definitions, options, context),
- kanjiShow: ({id, elementRect, writingMode, definitions, options, context}) => this.kanjiShow(id, elementRect, writingMode, definitions, options, context),
+ showContent: ({id, elementRect, writingMode, type, details}) => this.showContent(id, elementRect, writingMode, type, details),
+ setCustomCss: ({id, css}) => this.setCustomCss(id, css),
clearAutoPlayTimer: ({id}) => this.clearAutoPlayTimer(id)
});
}
- createPopup(parentId) {
+ createPopup(parentId, depth) {
const parent = (typeof parentId === 'string' && this.popups.hasOwnProperty(parentId) ? this.popups[parentId] : null);
- const depth = (parent !== null ? parent.depth + 1 : 0);
const id = `${this.nextId}`;
+ if (parent !== null) {
+ depth = parent.depth + 1;
+ }
++this.nextId;
const popup = new Popup(id, depth, this.frameIdPromise);
if (parent !== null) {
@@ -64,7 +66,7 @@ class PopupProxyHost {
}
async createNestedPopup(parentId) {
- return this.createPopup(parentId).id;
+ return this.createPopup(parentId, 0).id;
}
getPopup(id) {
@@ -86,26 +88,24 @@ class PopupProxyHost {
return new DOMRect(x, y, jsonRect.width, jsonRect.height);
}
- async show(id, elementRect, options) {
+ async setOptions(id, options) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- return await popup.show(elementRect, options);
+ return await popup.setOptions(options);
}
- async showOrphaned(id, elementRect, options) {
+ async hide(id, changeFocus) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- return await popup.showOrphaned(elementRect, options);
+ return popup.hide(changeFocus);
}
- async hide(id, changeFocus) {
+ async isVisibleAsync(id) {
const popup = this.getPopup(id);
- return popup.hide(changeFocus);
+ return await popup.isVisibleAsync();
}
async setVisibleOverride(id, visible) {
const popup = this.getPopup(id);
- return popup.setVisibleOverride(visible);
+ return await popup.setVisibleOverride(visible);
}
async containsPoint(id, x, y) {
@@ -113,18 +113,16 @@ class PopupProxyHost {
return await popup.containsPoint(x, y);
}
- async termsShow(id, elementRect, writingMode, definitions, options, context) {
+ async showContent(id, elementRect, writingMode, type, details) {
const popup = this.getPopup(id);
elementRect = this.jsonRectToDOMRect(popup, elementRect);
- if (!PopupProxyHost.popupCanShow(popup)) { return false; }
- return await popup.termsShow(elementRect, writingMode, definitions, options, context);
+ if (!PopupProxyHost.popupCanShow(popup)) { return Promise.resolve(false); }
+ return await popup.showContent(elementRect, writingMode, type, details);
}
- async kanjiShow(id, elementRect, writingMode, definitions, options, context) {
+ async setCustomCss(id, css) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- if (!PopupProxyHost.popupCanShow(popup)) { return false; }
- return await popup.kanjiShow(elementRect, writingMode, definitions, options, context);
+ return popup.setCustomCss(css);
}
async clearAutoPlayTimer(id) {
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index efbd28b2..e62a4868 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -46,16 +46,9 @@ class PopupProxy {
return id;
}
- async show(elementRect, options) {
+ async setOptions(options) {
const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('show', {id, elementRect, options});
- }
-
- async showOrphaned(elementRect, options) {
- const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('showOrphaned', {id, elementRect, options});
+ return await this.invokeHostApi('setOptions', {id, options});
}
async hide(changeFocus) {
@@ -65,6 +58,11 @@ class PopupProxy {
return await this.invokeHostApi('hide', {id: this.id, changeFocus});
}
+ async isVisibleAsync() {
+ const id = await this.getPopupId();
+ return await this.invokeHostApi('isVisibleAsync', {id});
+ }
+
async setVisibleOverride(visible) {
const id = await this.getPopupId();
return await this.invokeHostApi('setVisibleOverride', {id, visible});
@@ -77,16 +75,15 @@ class PopupProxy {
return await this.invokeHostApi('containsPoint', {id: this.id, x, y});
}
- async termsShow(elementRect, writingMode, definitions, options, context) {
+ async showContent(elementRect, writingMode, type=null, details=null) {
const id = await this.getPopupId();
elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('termsShow', {id, elementRect, writingMode, definitions, options, context});
+ return await this.invokeHostApi('showContent', {id, elementRect, writingMode, type, details});
}
- async kanjiShow(elementRect, writingMode, definitions, options, context) {
+ async setCustomCss(css) {
const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('kanjiShow', {id, elementRect, writingMode, definitions, options, context});
+ return await this.invokeHostApi('setCustomCss', {id, css});
}
async clearAutoPlayTimer() {
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 9ca91afa..b5eb9fe2 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -25,8 +25,9 @@ class Popup {
this.frameId = null;
this.parent = null;
this.child = null;
+ this.childrenSupported = true;
this.container = document.createElement('iframe');
- this.container.id = 'yomichan-float';
+ this.container.className = 'yomichan-float';
this.container.addEventListener('mousedown', e => e.stopPropagation());
this.container.addEventListener('scroll', e => e.stopPropagation());
this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html'));
@@ -36,17 +37,19 @@ class Popup {
this.isInjected = false;
this.visible = false;
this.visibleOverride = null;
+ this.options = null;
+ this.stylesheetInjectedViaApi = false;
this.updateVisibility();
}
- inject(options) {
+ inject() {
if (this.injectPromise === null) {
- this.injectPromise = this.createInjectPromise(options);
+ this.injectPromise = this.createInjectPromise();
}
return this.injectPromise;
}
- async createInjectPromise(options) {
+ async createInjectPromise() {
try {
const {frameId} = await this.frameIdPromise;
if (typeof frameId === 'number') {
@@ -60,30 +63,44 @@ class Popup {
const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null);
this.container.addEventListener('load', () => {
this.invokeApi('initialize', {
- options: {
- general: {
- customPopupCss: options.general.customPopupCss
- }
- },
+ options: this.options,
popupInfo: {
id: this.id,
depth: this.depth,
parentFrameId
},
- url: this.url
+ url: this.url,
+ childrenSupported: this.childrenSupported
});
resolve();
});
this.observeFullscreen();
this.onFullscreenChanged();
+ this.setCustomOuterCss(this.options.general.customPopupOuterCss, false);
this.isInjected = true;
});
}
- async show(elementRect, writingMode, options) {
- await this.inject(options);
+ isInitialized() {
+ return this.options !== null;
+ }
+
+ async setOptions(options) {
+ this.options = options;
+ this.updateTheme();
+ }
+
+ async showContent(elementRect, writingMode, type=null, details=null) {
+ if (!this.isInitialized()) { return; }
+ await this.show(elementRect, writingMode);
+ if (type === null) { return; }
+ this.invokeApi('setContent', {type, details});
+ }
- const optionsGeneral = options.general;
+ async show(elementRect, writingMode) {
+ await this.inject();
+
+ const optionsGeneral = this.options.general;
const container = this.container;
const containerRect = container.getBoundingClientRect();
const getPosition = (
@@ -208,11 +225,6 @@ class Popup {
return [position, size, after];
}
- async showOrphaned(elementRect, writingMode, options) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('orphaned');
- }
-
hide(changeFocus) {
if (!this.isVisible()) {
return;
@@ -227,6 +239,10 @@ class Popup {
}
}
+ async isVisibleAsync() {
+ return this.isVisible();
+ }
+
isVisible() {
return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible);
}
@@ -261,6 +277,44 @@ class Popup {
}
}
+ updateTheme() {
+ this.container.dataset.yomichanTheme = this.options.general.popupOuterTheme;
+ this.container.dataset.yomichanSiteColor = this.getSiteColor();
+ }
+
+ getSiteColor() {
+ const color = [255, 255, 255];
+ Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor));
+ Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.body).backgroundColor));
+ const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128);
+ return dark ? 'dark' : 'light';
+ }
+
+ static addColor(target, color) {
+ if (color === null) { return; }
+
+ const a = color[3];
+ if (a <= 0.0) { return; }
+
+ const aInv = 1.0 - a;
+ for (let i = 0; i < 3; ++i) {
+ target[i] = target[i] * aInv + color[i] * a;
+ }
+ }
+
+ static getColorInfo(cssColor) {
+ const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d\.]+)\s*)?\)\s*$/.exec(cssColor);
+ if (m === null) { return null; }
+
+ const m4 = m[4];
+ return [
+ Number.parseInt(m[1], 10),
+ Number.parseInt(m[2], 10),
+ Number.parseInt(m[3], 10),
+ m4 ? Math.max(0.0, Math.min(1.0, Number.parseFloat(m4))) : 1.0
+ ];
+ }
+
async containsPoint(x, y) {
for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) {
const rect = popup.container.getBoundingClientRect();
@@ -271,14 +325,25 @@ class Popup {
return false;
}
- async termsShow(elementRect, writingMode, definitions, options, context) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('termsShow', {definitions, options, context});
+ async setCustomCss(css) {
+ this.invokeApi('setCustomCss', {css});
}
- async kanjiShow(elementRect, writingMode, definitions, options, context) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('kanjiShow', {definitions, options, context});
+ async setCustomOuterCss(css, injectDirectly) {
+ // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them.
+ if (this.stylesheetInjectedViaApi) { return; }
+
+ if (injectDirectly || Popup.isOnExtensionPage()) {
+ Popup.injectOuterStylesheet(css);
+ } else {
+ if (!css) { return; }
+ try {
+ await apiInjectStylesheet(css);
+ this.stylesheetInjectedViaApi = true;
+ } catch (e) {
+ // NOP
+ }
+ }
}
clearAutoPlayTimer() {
@@ -322,4 +387,35 @@ class Popup {
get url() {
return window.location.href;
}
+
+ static isOnExtensionPage() {
+ try {
+ const url = chrome.runtime.getURL('/');
+ return window.location.href.substr(0, url.length) === url;
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static injectOuterStylesheet(css) {
+ if (Popup.outerStylesheet === null) {
+ if (!css) { return; }
+ Popup.outerStylesheet = document.createElement('style');
+ Popup.outerStylesheet.id = "yomichan-popup-outer-stylesheet";
+ }
+
+ const outerStylesheet = Popup.outerStylesheet;
+ if (css) {
+ outerStylesheet.textContent = css;
+
+ const par = document.head;
+ if (par && outerStylesheet.parentNode !== par) {
+ par.appendChild(outerStylesheet);
+ }
+ } else {
+ outerStylesheet.textContent = '';
+ }
+ }
}
+
+Popup.outerStylesheet = null;
diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js
index ee4f58e2..c3da9f46 100644
--- a/ext/fg/js/source.js
+++ b/ext/fg/js/source.js
@@ -229,13 +229,29 @@ class TextSourceRange {
}
static getElementWritingMode(element) {
- if (element === null) {
- return 'horizontal-tb';
+ if (element !== null) {
+ const style = window.getComputedStyle(element);
+ const writingMode = style.writingMode;
+ if (typeof writingMode === 'string') {
+ return TextSourceRange.normalizeWritingMode(writingMode);
+ }
}
+ return 'horizontal-tb';
+ }
- const style = window.getComputedStyle(element);
- const writingMode = style.writingMode;
- return typeof writingMode === 'string' ? writingMode : 'horizontal-tb';
+ static normalizeWritingMode(writingMode) {
+ switch (writingMode) {
+ case 'lr':
+ case 'lr-tb':
+ case 'rl':
+ return 'horizontal-tb';
+ case 'tb':
+ return 'vertical-lr';
+ case 'tb-rl':
+ return 'vertical-rl';
+ default:
+ return writingMode;
+ }
}
static getNodesInRange(range) {