aboutsummaryrefslogtreecommitdiff
path: root/ext/fg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg')
-rw-r--r--ext/fg/js/frame-offset-forwarder.js102
-rw-r--r--ext/fg/js/frontend-initialize.js30
-rw-r--r--ext/fg/js/frontend.js11
-rw-r--r--ext/fg/js/popup-proxy-host.js12
-rw-r--r--ext/fg/js/popup-proxy.js104
-rw-r--r--ext/fg/js/popup.js83
6 files changed, 244 insertions, 98 deletions
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 3a191247..4a1409db 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -17,9 +17,12 @@
*/
/* global
+ * FrameOffsetForwarder
* Frontend
* PopupProxy
* PopupProxyHost
+ * apiForward
+ * apiOptionsGet
*/
async function main() {
@@ -28,10 +31,35 @@ async function main() {
const data = window.frontendInitializationData || {};
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();
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index d6c5eac6..4e9d474c 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -18,6 +18,7 @@
/* global
* TextScanner
+ * apiForward
* apiGetZoom
* apiKanjiFind
* apiOptionsGet
@@ -52,7 +53,8 @@ class Frontend extends TextScanner {
]);
this._runtimeMessageHandlers = new Map([
- ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }]
+ ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }],
+ ['rootPopupRequestInformationBroadcast', () => { this._broadcastRootPopupInformation(); }]
]);
}
@@ -76,6 +78,7 @@ class Frontend extends TextScanner {
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
this._updateContentScale();
+ this._broadcastRootPopupInformation();
} catch (e) {
this.onError(e);
}
@@ -255,6 +258,12 @@ 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});
+ }
+ }
+
async _updatePopupPosition() {
const textSource = this.getCurrentTextSource();
if (textSource !== null && await this.popup.isVisible()) {
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index 6f1c13c6..4b136e41 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -26,17 +26,17 @@ class PopupProxyHost {
constructor() {
this._popups = new Map();
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)],
@@ -87,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);
}
@@ -95,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 997b1317..966198a9 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -21,14 +21,18 @@
*/
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
@@ -51,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 e6e93a76..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,40 +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', () => {
- this._listenForDisplayPrepareCompleted(resolve);
-
- this._invokeApi('prepare', {
- popupInfo: {
- id: this._id,
- depth: this._depth,
- parentFrameId
- },
- url: this.url,
- childrenSupported: this._childrenSupported,
- scale: this._contentScale
- });
+ 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() {
@@ -361,22 +366,6 @@ class Popup {
contentWindow.postMessage({action, params, token}, this._targetOrigin);
}
- _listenForDisplayPrepareCompleted(resolve) {
- const runtimeMessageCallback = ({action, params}, sender, callback) => {
- if (
- action === 'popupPrepareCompleted' &&
- isObject(params) &&
- params.targetPopupId === this._id
- ) {
- chrome.runtime.onMessage.removeListener(runtimeMessageCallback);
- callback();
- resolve();
- return false;
- }
- };
- chrome.runtime.onMessage.addListener(runtimeMessageCallback);
- }
-
static _getFullscreenElement() {
return (
document.fullscreenElement ||