summaryrefslogtreecommitdiff
path: root/ext/fg/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg/js')
-rw-r--r--ext/fg/js/float.js44
-rw-r--r--ext/fg/js/frame-offset-forwarder.js102
-rw-r--r--ext/fg/js/frontend-initialize.js34
-rw-r--r--ext/fg/js/frontend.js38
-rw-r--r--ext/fg/js/popup-nested.js7
-rw-r--r--ext/fg/js/popup-proxy-host.js15
-rw-r--r--ext/fg/js/popup-proxy.js108
-rw-r--r--ext/fg/js/popup.js87
8 files changed, 312 insertions, 123 deletions
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 393c2719..01055ca6 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -28,6 +28,8 @@ class DisplayFloat extends Display {
super(document.querySelector('#spinner'), document.querySelector('#definitions'));
this.autoPlayAudioTimer = null;
+ this._popupId = null;
+
this.optionsContext = {
depth: 0,
url: window.location.href
@@ -53,7 +55,7 @@ class DisplayFloat extends Display {
['setContent', ({type, details}) => this.setContent(type, details)],
['clearAutoPlayTimer', () => this.clearAutoPlayTimer()],
['setCustomCss', ({css}) => this.setCustomCss(css)],
- ['prepare', ({options, popupInfo, url, childrenSupported, scale, uniqueId}) => this.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],
+ ['prepare', ({popupInfo, url, childrenSupported, scale}) => this.prepare(popupInfo, url, childrenSupported, scale)],
['setContentScale', ({scale}) => this.setContentScale(scale)]
]);
@@ -61,23 +63,24 @@ class DisplayFloat extends Display {
window.addEventListener('message', this.onMessage.bind(this), false);
}
- async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) {
+ async prepare(popupInfo, url, childrenSupported, scale) {
if (this._prepareInvoked) { return; }
this._prepareInvoked = true;
- await super.prepare(options);
-
const {id, depth, parentFrameId} = popupInfo;
+ this._popupId = id;
this.optionsContext.depth = depth;
this.optionsContext.url = url;
+ await super.prepare();
+
if (childrenSupported) {
popupNestedInitialize(id, depth, parentFrameId, url);
}
this.setContentScale(scale);
- apiForward('popupPrepareCompleted', {uniqueId});
+ apiForward('popupPrepareCompleted', {targetPopupId: this._popupId});
}
onError(error) {
@@ -144,10 +147,6 @@ class DisplayFloat extends Display {
handler(params);
}
- getOptionsContext() {
- return this.optionsContext;
- }
-
autoPlayAudio() {
this.clearAutoPlayTimer();
this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400);
@@ -163,6 +162,33 @@ class DisplayFloat extends Display {
setContentScale(scale) {
document.body.style.fontSize = `${scale}em`;
}
+
+ async getDocumentTitle() {
+ try {
+ const uniqueId = yomichan.generateId(16);
+
+ const promise = yomichan.getTemporaryListenerResult(
+ chrome.runtime.onMessage,
+ ({action, params}, {resolve}) => {
+ if (
+ action === 'documentInformationBroadcast' &&
+ isObject(params) &&
+ params.uniqueId === uniqueId &&
+ params.frameId === 0
+ ) {
+ resolve(params);
+ }
+ },
+ 2000
+ );
+ apiForward('requestDocumentInformationBroadcast', {uniqueId});
+
+ const {title} = await promise;
+ return title;
+ } catch (e) {
+ return '';
+ }
+ }
}
DisplayFloat.instance = new DisplayFloat();
diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js
new file mode 100644
index 00000000..7b417b6e
--- /dev/null
+++ b/ext/fg/js/frame-offset-forwarder.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * apiForward
+ */
+
+class FrameOffsetForwarder {
+ constructor() {
+ this._started = false;
+
+ this._forwardFrameOffset = (
+ window !== window.parent ?
+ this._forwardFrameOffsetParent.bind(this) :
+ this._forwardFrameOffsetOrigin.bind(this)
+ );
+
+ this._windowMessageHandlers = new Map([
+ ['getFrameOffset', ({offset, uniqueId}, e) => this._onGetFrameOffset(offset, uniqueId, e)]
+ ]);
+ }
+
+ start() {
+ if (this._started) { return; }
+ window.addEventListener('message', this.onMessage.bind(this), false);
+ this._started = true;
+ }
+
+ async getOffset() {
+ const uniqueId = yomichan.generateId(16);
+
+ const frameOffsetPromise = yomichan.getTemporaryListenerResult(
+ chrome.runtime.onMessage,
+ ({action, params}, {resolve}) => {
+ if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) {
+ resolve(params);
+ }
+ },
+ 5000
+ );
+
+ window.parent.postMessage({
+ action: 'getFrameOffset',
+ params: {
+ uniqueId,
+ offset: [0, 0]
+ }
+ }, '*');
+
+ const {offset} = await frameOffsetPromise;
+ return offset;
+ }
+
+ onMessage(e) {
+ const {action, params} = e.data;
+ const handler = this._windowMessageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+ handler(params, e);
+ }
+
+ _onGetFrameOffset(offset, uniqueId, e) {
+ let sourceFrame = null;
+ for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) {
+ if (frame.contentWindow !== e.source) { continue; }
+ sourceFrame = frame;
+ break;
+ }
+ if (sourceFrame === null) {
+ this._forwardFrameOffsetOrigin(null, uniqueId);
+ return;
+ }
+
+ const [forwardedX, forwardedY] = offset;
+ const {x, y} = sourceFrame.getBoundingClientRect();
+ offset = [forwardedX + x, forwardedY + y];
+
+ this._forwardFrameOffset(offset, uniqueId);
+ }
+
+ _forwardFrameOffsetParent(offset, uniqueId) {
+ window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*');
+ }
+
+ _forwardFrameOffsetOrigin(offset, uniqueId) {
+ apiForward('frameOffset', {offset, uniqueId});
+ }
+}
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
index 8424b21d..4a1409db 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -17,28 +17,56 @@
*/
/* global
+ * FrameOffsetForwarder
* Frontend
* PopupProxy
* PopupProxyHost
+ * apiForward
+ * apiOptionsGet
*/
async function main() {
await yomichan.prepare();
const data = window.frontendInitializationData || {};
- const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
+ const {id, depth=0, parentFrameId, url, proxy=false} = data;
+
+ const optionsContext = {depth, url};
+ const options = await apiOptionsGet(optionsContext);
let popup;
- if (proxy) {
+ if (!proxy && (window !== window.parent) && options.general.showIframePopupsInRootFrame) {
+ const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
+ chrome.runtime.onMessage,
+ ({action, params}, {resolve}) => {
+ if (action === 'rootPopupInformation') {
+ resolve(params);
+ }
+ }
+ );
+ apiForward('rootPopupRequestInformationBroadcast');
+ const {popupId, frameId} = await rootPopupInformationPromise;
+
+ const frameOffsetForwarder = new FrameOffsetForwarder();
+ frameOffsetForwarder.start();
+ const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder);
+
+ popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset);
+ await popup.prepare();
+ } else if (proxy) {
popup = new PopupProxy(null, depth + 1, id, parentFrameId, url);
+ await popup.prepare();
} else {
+ const frameOffsetForwarder = new FrameOffsetForwarder();
+ frameOffsetForwarder.start();
+
const popupHost = new PopupProxyHost();
await popupHost.prepare();
popup = popupHost.getOrCreatePopup(null, null, depth);
}
- const frontend = new Frontend(popup, ignoreNodes);
+ const frontend = new Frontend(popup);
await frontend.prepare();
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 768b9326..31843212 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -18,6 +18,7 @@
/* global
* TextScanner
+ * apiForward
* apiGetZoom
* apiKanjiFind
* apiOptionsGet
@@ -26,10 +27,9 @@
*/
class Frontend extends TextScanner {
- constructor(popup, ignoreNodes) {
+ constructor(popup) {
super(
window,
- ignoreNodes,
popup.isProxy() ? [] : [popup.getContainer()],
[(x, y) => this.popup.containsPoint(x, y)]
);
@@ -53,7 +53,9 @@ class Frontend extends TextScanner {
]);
this._runtimeMessageHandlers = new Map([
- ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }]
+ ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }],
+ ['rootPopupRequestInformationBroadcast', () => { this._broadcastRootPopupInformation(); }],
+ ['requestDocumentInformationBroadcast', ({uniqueId}) => { this._broadcastDocumentInformation(uniqueId); }]
]);
}
@@ -77,6 +79,7 @@ class Frontend extends TextScanner {
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
this._updateContentScale();
+ this._broadcastRootPopupInformation();
} catch (e) {
this.onError(e);
}
@@ -95,6 +98,9 @@ class Frontend extends TextScanner {
}
onRuntimeMessage({action, params}, sender, callback) {
+ const {targetPopupId} = params || {};
+ if (typeof targetPopupId !== 'undefined' && targetPopupId !== this.popup.id) { return; }
+
const handler = this._runtimeMessageHandlers.get(action);
if (typeof handler !== 'function') { return false; }
@@ -129,8 +135,20 @@ class Frontend extends TextScanner {
async updateOptions() {
this.setOptions(await apiOptionsGet(this.getOptionsContext()));
+
+ const ignoreNodes = ['.scan-disable', '.scan-disable *'];
+ if (!this.options.scanning.enableOnPopupExpressions) {
+ ignoreNodes.push('.source-text', '.source-text *');
+ }
+ this.ignoreNodes = ignoreNodes.join(',');
+
await this.popup.setOptions(this.options);
+
this._updateContentScale();
+
+ if (this.textSourceCurrent !== null && this.causeCurrent !== null) {
+ await this.onSearchSource(this.textSourceCurrent, this.causeCurrent);
+ }
}
async onSearchSource(textSource, cause) {
@@ -241,6 +259,20 @@ class Frontend extends TextScanner {
this._updatePopupPosition();
}
+ _broadcastRootPopupInformation() {
+ if (!this.popup.isProxy() && this.popup.depth === 0) {
+ apiForward('rootPopupInformation', {popupId: this.popup.id, frameId: this.popup.frameId});
+ }
+ }
+
+ _broadcastDocumentInformation(uniqueId) {
+ apiForward('documentInformationBroadcast', {
+ uniqueId,
+ frameId: this.popup.frameId,
+ title: document.title
+ });
+ }
+
async _updatePopupPosition() {
const textSource = this.getCurrentTextSource();
if (textSource !== null && await this.popup.isVisible()) {
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index 06f8fc4b..39d91fd8 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -36,12 +36,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
return;
}
- const ignoreNodes = ['.scan-disable', '.scan-disable *'];
- if (!options.scanning.enableOnPopupExpressions) {
- ignoreNodes.push('.source-text', '.source-text *');
- }
-
- window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true};
+ window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true};
const scriptSrcs = [
'/mixed/js/text-scanner.js',
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index 793d3949..4b136e41 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -25,19 +25,18 @@
class PopupProxyHost {
constructor() {
this._popups = new Map();
- this._nextId = 0;
this._apiReceiver = null;
- this._frameIdPromise = null;
+ this._frameId = null;
}
// Public functions
async prepare() {
- this._frameIdPromise = apiFrameInformationGet();
- const {frameId} = await this._frameIdPromise;
+ const {frameId} = await apiFrameInformationGet();
if (typeof frameId !== 'number') { return; }
+ this._frameId = frameId;
- this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([
+ this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([
['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)],
['setOptions', this._onApiSetOptions.bind(this)],
['hide', this._onApiHide.bind(this)],
@@ -76,7 +75,7 @@ class PopupProxyHost {
// New unique id
if (id === null) {
- id = this._nextId++;
+ id = yomichan.generateId(16);
}
// Create new popup
@@ -88,7 +87,7 @@ class PopupProxyHost {
} else if (depth === null) {
depth = 0;
}
- const popup = new Popup(id, depth, this._frameIdPromise);
+ const popup = new Popup(id, depth, this._frameId);
if (parent !== null) {
popup.setParent(parent);
}
@@ -96,7 +95,7 @@ class PopupProxyHost {
return popup;
}
- // Message handlers
+ // API message handlers
async _onApiGetOrCreatePopup({id, parentId}) {
const popup = this.getOrCreatePopup(id, parentId);
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index f7cef214..966198a9 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -21,18 +21,26 @@
*/
class PopupProxy {
- constructor(id, depth, parentId, parentFrameId, url) {
+ constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) {
this._parentId = parentId;
this._parentFrameId = parentFrameId;
this._id = id;
- this._idPromise = null;
this._depth = depth;
this._url = url;
this._apiSender = new FrontendApiSender();
+ this._getFrameOffset = getFrameOffset;
+
+ this._frameOffset = null;
+ this._frameOffsetPromise = null;
+ this._frameOffsetUpdatedAt = null;
}
// Public properties
+ get id() {
+ return this._id;
+ }
+
get parent() {
return null;
}
@@ -47,92 +55,106 @@ class PopupProxy {
// Public functions
+ async prepare() {
+ const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId});
+ this._id = id;
+ }
+
isProxy() {
return true;
}
async setOptions(options) {
- const id = await this._getPopupId();
- return await this._invokeHostApi('setOptions', {id, options});
+ return await this._invokeHostApi('setOptions', {id: this._id, options});
}
hide(changeFocus) {
- if (this._id === null) {
- return;
- }
this._invokeHostApi('hide', {id: this._id, changeFocus});
}
async isVisible() {
- const id = await this._getPopupId();
- return await this._invokeHostApi('isVisible', {id});
+ return await this._invokeHostApi('isVisible', {id: this._id});
}
setVisibleOverride(visible) {
- if (this._id === null) {
- return;
- }
this._invokeHostApi('setVisibleOverride', {id: this._id, visible});
}
async containsPoint(x, y) {
- if (this._id === null) {
- return false;
+ if (this._getFrameOffset !== null) {
+ await this._updateFrameOffset();
+ [x, y] = this._applyFrameOffset(x, y);
}
return await this._invokeHostApi('containsPoint', {id: this._id, x, y});
}
async showContent(elementRect, writingMode, type=null, details=null) {
- const id = await this._getPopupId();
- elementRect = PopupProxy._convertDOMRectToJson(elementRect);
- return await this._invokeHostApi('showContent', {id, elementRect, writingMode, type, details});
+ let {x, y, width, height} = elementRect;
+ if (this._getFrameOffset !== null) {
+ await this._updateFrameOffset();
+ [x, y] = this._applyFrameOffset(x, y);
+ }
+ elementRect = {x, y, width, height};
+ return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details});
}
async setCustomCss(css) {
- const id = await this._getPopupId();
- return await this._invokeHostApi('setCustomCss', {id, css});
+ return await this._invokeHostApi('setCustomCss', {id: this._id, css});
}
clearAutoPlayTimer() {
- if (this._id === null) {
- return;
- }
this._invokeHostApi('clearAutoPlayTimer', {id: this._id});
}
async setContentScale(scale) {
- const id = await this._getPopupId();
- this._invokeHostApi('setContentScale', {id, scale});
+ this._invokeHostApi('setContentScale', {id: this._id, scale});
}
// Private
- _getPopupId() {
- if (this._idPromise === null) {
- this._idPromise = this._getPopupIdAsync();
+ _invokeHostApi(action, params={}) {
+ if (typeof this._parentFrameId !== 'number') {
+ return Promise.reject(new Error('Invalid frame'));
}
- return this._idPromise;
+ return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`);
}
- async _getPopupIdAsync() {
- const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId});
- this._id = id;
- return id;
+ async _updateFrameOffset() {
+ const now = Date.now();
+ const firstRun = this._frameOffsetUpdatedAt === null;
+ const expired = firstRun || this._frameOffsetUpdatedAt < now - PopupProxy._frameOffsetExpireTimeout;
+ if (this._frameOffsetPromise === null && !expired) { return; }
+
+ if (this._frameOffsetPromise !== null) {
+ if (firstRun) {
+ await this._frameOffsetPromise;
+ }
+ return;
+ }
+
+ const promise = this._updateFrameOffsetInner(now);
+ if (firstRun) {
+ await promise;
+ }
}
- _invokeHostApi(action, params={}) {
- if (typeof this._parentFrameId !== 'number') {
- return Promise.reject(new Error('Invalid frame'));
+ async _updateFrameOffsetInner(now) {
+ this._frameOffsetPromise = this._getFrameOffset();
+ try {
+ const offset = await this._frameOffsetPromise;
+ this._frameOffset = offset !== null ? offset : [0, 0];
+ this._frameOffsetUpdatedAt = now;
+ } catch (e) {
+ logError(e);
+ } finally {
+ this._frameOffsetPromise = null;
}
- return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`);
}
- static _convertDOMRectToJson(domRect) {
- return {
- x: domRect.x,
- y: domRect.y,
- width: domRect.width,
- height: domRect.height
- };
+ _applyFrameOffset(x, y) {
+ const [offsetX, offsetY] = this._frameOffset;
+ return [x + offsetX, y + offsetY];
}
}
+
+PopupProxy._frameOffsetExpireTimeout = 1000;
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index d752812e..60dc16dd 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -22,11 +22,10 @@
*/
class Popup {
- constructor(id, depth, frameIdPromise) {
+ constructor(id, depth, frameId) {
this._id = id;
this._depth = depth;
- this._frameIdPromise = frameIdPromise;
- this._frameId = null;
+ this._frameId = frameId;
this._parent = null;
this._child = null;
this._childrenSupported = true;
@@ -69,6 +68,10 @@ class Popup {
return this._depth;
}
+ get frameId() {
+ return this._frameId;
+ }
+
get url() {
return window.location.href;
}
@@ -193,43 +196,42 @@ class Popup {
}
async _createInjectPromise() {
- try {
- const {frameId} = await this._frameIdPromise;
- if (typeof frameId === 'number') {
- this._frameId = frameId;
- }
- } catch (e) {
- // NOP
- }
-
if (this._messageToken === null) {
this._messageToken = await apiGetMessageToken();
}
- return new Promise((resolve) => {
- const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);
- this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
- this._container.addEventListener('load', () => {
- const uniqueId = yomichan.generateId(32);
- Popup._listenForDisplayPrepareCompleted(uniqueId, resolve);
-
- this._invokeApi('prepare', {
- options: this._options,
- popupInfo: {
- id: this._id,
- depth: this._depth,
- parentFrameId
- },
- url: this.url,
- childrenSupported: this._childrenSupported,
- scale: this._contentScale,
- uniqueId
- });
+ const popupPreparedPromise = yomichan.getTemporaryListenerResult(
+ chrome.runtime.onMessage,
+ ({action, params}, {resolve}) => {
+ if (
+ action === 'popupPrepareCompleted' &&
+ isObject(params) &&
+ params.targetPopupId === this._id
+ ) {
+ resolve();
+ }
+ }
+ );
+
+ const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);
+ this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
+ this._container.addEventListener('load', () => {
+ this._invokeApi('prepare', {
+ popupInfo: {
+ id: this._id,
+ depth: this._depth,
+ parentFrameId
+ },
+ url: this.url,
+ childrenSupported: this._childrenSupported,
+ scale: this._contentScale
});
- this._observeFullscreen(true);
- this._onFullscreenChanged();
- this._injectStyles();
});
+ this._observeFullscreen(true);
+ this._onFullscreenChanged();
+ this._injectStyles();
+
+ return popupPreparedPromise;
}
async _injectStyles() {
@@ -374,23 +376,6 @@ class Popup {
);
}
- static _listenForDisplayPrepareCompleted(uniqueId, resolve) {
- const runtimeMessageCallback = ({action, params}, sender, callback) => {
- if (
- action === 'popupPrepareCompleted' &&
- typeof params === 'object' &&
- params !== null &&
- params.uniqueId === uniqueId
- ) {
- chrome.runtime.onMessage.removeListener(runtimeMessageCallback);
- callback();
- resolve();
- return false;
- }
- };
- chrome.runtime.onMessage.addListener(runtimeMessageCallback);
- }
-
static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {
const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below');
const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale;