From b936c3e4b1bc993e535b02dee91bf6afc15a3564 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 8 May 2020 19:04:53 -0400 Subject: Popup proxy host refactor (#516) * Rename PopupProxyHost to PopupFactory * Update FrontendApiReceiver to support non-async handlers * Make some functions non-async * Make setCustomCss non-async * Make setContentScale non-async * Remove static * Rename variables * Pass frameId into PopupFactory's constructor * Change FrontendApiReceiver source from popup-proxy-host to popup-factor * Rename _invokeHostApi to _invoke * Rename PopupProxy.getHostUrl to getUrl --- ext/fg/js/popup-factory.js | 181 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 ext/fg/js/popup-factory.js (limited to 'ext/fg/js/popup-factory.js') diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js new file mode 100644 index 00000000..21e64dd0 --- /dev/null +++ b/ext/fg/js/popup-factory.js @@ -0,0 +1,181 @@ +/* + * 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 . + */ + +/* global + * FrontendApiReceiver + * Popup + */ + +class PopupFactory { + constructor(frameId) { + this._popups = new Map(); + this._frameId = frameId; + } + + // Public functions + + async prepare() { + const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([ + ['getOrCreatePopup', {async: false, handler: this._onApiGetOrCreatePopup.bind(this)}], + ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], + ['hide', {async: false, handler: this._onApiHide.bind(this)}], + ['isVisible', {async: true, handler: this._onApiIsVisibleAsync.bind(this)}], + ['setVisibleOverride', {async: true, handler: this._onApiSetVisibleOverride.bind(this)}], + ['containsPoint', {async: true, handler: this._onApiContainsPoint.bind(this)}], + ['showContent', {async: true, handler: this._onApiShowContent.bind(this)}], + ['setCustomCss', {async: false, handler: this._onApiSetCustomCss.bind(this)}], + ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], + ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], + ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] + ])); + apiReceiver.prepare(); + } + + getOrCreatePopup(id=null, parentId=null, depth=null) { + // Find by existing id + if (id !== null) { + const popup = this._popups.get(id); + if (typeof popup !== 'undefined') { + return popup; + } + } + + // Find by existing parent id + let parent = null; + if (parentId !== null) { + parent = this._popups.get(parentId); + if (typeof parent !== 'undefined') { + const popup = parent.child; + if (popup !== null) { + return popup; + } + } else { + parent = null; + } + } + + // New unique id + if (id === null) { + id = yomichan.generateId(16); + } + + // Create new popup + if (parent !== null) { + if (depth !== null) { + throw new Error('Depth cannot be set when parent exists'); + } + depth = parent.depth + 1; + } else if (depth === null) { + depth = 0; + } + const popup = new Popup(id, depth, this._frameId); + if (parent !== null) { + popup.setParent(parent); + } + this._popups.set(id, popup); + return popup; + } + + // API message handlers + + _onApiGetOrCreatePopup({id, parentId}) { + const popup = this.getOrCreatePopup(id, parentId); + return { + id: popup.id + }; + } + + async _onApiSetOptionsContext({id, optionsContext, source}) { + const popup = this._getPopup(id); + return await popup.setOptionsContext(optionsContext, source); + } + + _onApiHide({id, changeFocus}) { + const popup = this._getPopup(id); + return popup.hide(changeFocus); + } + + async _onApiIsVisibleAsync({id}) { + const popup = this._getPopup(id); + return await popup.isVisible(); + } + + async _onApiSetVisibleOverride({id, visible}) { + const popup = this._getPopup(id); + return await popup.setVisibleOverride(visible); + } + + async _onApiContainsPoint({id, x, y}) { + const popup = this._getPopup(id); + [x, y] = this._convertPopupPointToRootPagePoint(popup, x, y); + return await popup.containsPoint(x, y); + } + + async _onApiShowContent({id, elementRect, writingMode, type, details, context}) { + const popup = this._getPopup(id); + elementRect = this._convertJsonRectToDOMRect(popup, elementRect); + if (!this._popupCanShow(popup)) { return; } + return await popup.showContent(elementRect, writingMode, type, details, context); + } + + _onApiSetCustomCss({id, css}) { + const popup = this._getPopup(id); + return popup.setCustomCss(css); + } + + _onApiClearAutoPlayTimer({id}) { + const popup = this._getPopup(id); + return popup.clearAutoPlayTimer(); + } + + _onApiSetContentScale({id, scale}) { + const popup = this._getPopup(id); + return popup.setContentScale(scale); + } + + _onApiGetUrl() { + return window.location.href; + } + + // Private functions + + _getPopup(id) { + const popup = this._popups.get(id); + if (typeof popup === 'undefined') { + throw new Error(`Invalid popup ID ${id}`); + } + return popup; + } + + _convertJsonRectToDOMRect(popup, jsonRect) { + const [x, y] = this._convertPopupPointToRootPagePoint(popup, jsonRect.x, jsonRect.y); + return new DOMRect(x, y, jsonRect.width, jsonRect.height); + } + + _convertPopupPointToRootPagePoint(popup, x, y) { + if (popup.parent !== null) { + const popupRect = popup.parent.getContainerRect(); + x += popupRect.x; + y += popupRect.y; + } + return [x, y]; + } + + _popupCanShow(popup) { + return popup.parent === null || popup.parent.isVisibleSync(); + } +} -- cgit v1.2.3 From 48cf6469739b26d4157d79523ccea762ef90d6bd Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 8 May 2020 19:10:06 -0400 Subject: Popup refactor (#518) * Add default * Convert function to non-static * Remove static for private functions * Replace .call * Move functions with side effects into a synchronous prepare function * Rename variables with "container" to "frame" in _initializeFrame * Rename variables with "container" to "frame" * Rename getContainer to getFrame * Rename getContainerRect to getFrameRect * Organize and simplify * Fix incorrect change of "popup" => "this" * Move initial _updateVisibility into prepare() --- ext/fg/js/frontend.js | 2 +- ext/fg/js/popup-factory.js | 3 +- ext/fg/js/popup.js | 191 +++++++++++++++++++++++---------------------- 3 files changed, 102 insertions(+), 94 deletions(-) (limited to 'ext/fg/js/popup-factory.js') diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 78440991..1326f33f 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -41,7 +41,7 @@ class Frontend { this._optionsUpdatePending = false; this._textScanner = new TextScanner({ node: window, - ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getContainer()], + ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()], ignorePoint: (x, y) => this._popup.containsPoint(x, y), search: this._search.bind(this) }); diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index 21e64dd0..b10acbaf 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -87,6 +87,7 @@ class PopupFactory { popup.setParent(parent); } this._popups.set(id, popup); + popup.prepare(); return popup; } @@ -168,7 +169,7 @@ class PopupFactory { _convertPopupPointToRootPagePoint(popup, x, y) { if (popup.parent !== null) { - const popupRect = popup.parent.getContainerRect(); + const popupRect = popup.parent.getFrameRect(); x += popupRect.x; y += popupRect.y; } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 79b37251..c024bb64 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -36,23 +36,18 @@ class Popup { this._options = null; this._optionsContext = null; this._contentScale = 1.0; - this._containerSizeContentScale = null; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._previousOptionsContextSource = null; - this._containerSecret = null; - this._containerToken = null; - this._container = document.createElement('iframe'); - this._container.className = 'yomichan-float'; - this._container.addEventListener('mousedown', (e) => e.stopPropagation()); - this._container.addEventListener('scroll', (e) => e.stopPropagation()); - this._container.style.width = '0px'; - this._container.style.height = '0px'; - this._container.addEventListener('load', this._onFrameLoad.bind(this)); + this._frameSizeContentScale = null; + this._frameSecret = null; + this._frameToken = null; + this._frame = document.createElement('iframe'); + this._frame.className = 'yomichan-float'; + this._frame.style.width = '0'; + this._frame.style.height = '0'; this._fullscreenEventListeners = new EventListenerCollection(); - - this._updateVisibility(); } // Public properties @@ -79,6 +74,13 @@ class Popup { // Public functions + prepare() { + this._updateVisibility(); + this._frame.addEventListener('mousedown', (e) => e.stopPropagation()); + this._frame.addEventListener('scroll', (e) => e.stopPropagation()); + this._frame.addEventListener('load', this._onFrameLoad.bind(this)); + } + isProxy() { return false; } @@ -118,7 +120,7 @@ class Popup { async containsPoint(x, y) { for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup._child) { - const rect = popup._container.getBoundingClientRect(); + const rect = popup._frame.getBoundingClientRect(); if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { return true; } @@ -173,12 +175,12 @@ class Popup { } updateTheme() { - this._container.dataset.yomichanTheme = this._options.general.popupOuterTheme; - this._container.dataset.yomichanSiteColor = this._getSiteColor(); + this._frame.dataset.yomichanTheme = this._options.general.popupOuterTheme; + this._frame.dataset.yomichanSiteColor = this._getSiteColor(); } async setCustomOuterCss(css, useWebExtensionApi) { - return await Popup._injectStylesheet( + return await this._injectStylesheet( 'yomichan-popup-outer-user-stylesheet', 'code', css, @@ -190,12 +192,12 @@ class Popup { this._childrenSupported = value; } - getContainer() { - return this._container; + getFrame() { + return this._frame; } - getContainerRect() { - return this._container.getBoundingClientRect(); + getFrameRect() { + return this._frame.getBoundingClientRect(); } // Private functions @@ -220,11 +222,11 @@ class Popup { return new Promise((resolve, reject) => { const tokenMap = new Map(); let timer = null; - let containerLoadedResolve = null; - let containerLoadedReject = null; - const containerLoaded = new Promise((resolve2, reject2) => { - containerLoadedResolve = resolve2; - containerLoadedReject = reject2; + let frameLoadedResolve = null; + let frameLoadedReject = null; + const frameLoaded = new Promise((resolve2, reject2) => { + frameLoadedResolve = resolve2; + frameLoadedReject = reject2; }); const postMessage = (action, params) => { @@ -252,7 +254,7 @@ class Popup { if (!isObject(message)) { return; } const {action, params} = message; if (!isObject(params)) { return; } - await containerLoaded; + await frameLoaded; if (timer === null) { return; } // Done switch (action) { @@ -282,7 +284,7 @@ class Popup { }; const onLoad = () => { - if (containerLoadedResolve === null) { + if (frameLoadedResolve === null) { cleanup(); reject(new Error('Unexpected load event')); return; @@ -292,9 +294,9 @@ class Popup { return; } - containerLoadedResolve(); - containerLoadedResolve = null; - containerLoadedReject = null; + frameLoadedResolve(); + frameLoadedResolve = null; + frameLoadedReject = null; }; const cleanup = () => { @@ -302,10 +304,10 @@ class Popup { clearTimeout(timer); timer = null; - containerLoadedResolve = null; - if (containerLoadedReject !== null) { - containerLoadedReject(new Error('Terminated')); - containerLoadedReject = null; + frameLoadedResolve = null; + if (frameLoadedReject !== null) { + frameLoadedReject(new Error('Terminated')); + frameLoadedReject = null; } chrome.runtime.onMessage.removeListener(onMessage); @@ -322,7 +324,7 @@ class Popup { frame.addEventListener('load', onLoad); // Prevent unhandled rejections - containerLoaded.catch(() => {}); // NOP + frameLoaded.catch(() => {}); // NOP setupFrame(frame); }); @@ -331,15 +333,15 @@ class Popup { async _createInjectPromise() { this._injectStyles(); - const {secret, token} = await this._initializeFrame(this._container, this._targetOrigin, this._frameId, (frame) => { + const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => { frame.removeAttribute('src'); frame.removeAttribute('srcdoc'); frame.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); this._observeFullscreen(true); this._onFullscreenChanged(); }); - this._containerSecret = secret; - this._containerToken = token; + this._frameSecret = secret; + this._frameToken = token; // Configure const messageId = yomichan.generateId(16); @@ -374,22 +376,22 @@ class Popup { } _resetFrame() { - const parent = this._container.parentNode; + const parent = this._frame.parentNode; if (parent !== null) { - parent.removeChild(this._container); + parent.removeChild(this._frame); } - this._container.removeAttribute('src'); - this._container.removeAttribute('srcdoc'); + this._frame.removeAttribute('src'); + this._frame.removeAttribute('srcdoc'); - this._containerSecret = null; - this._containerToken = null; + this._frameSecret = null; + this._frameToken = null; this._injectPromise = null; this._injectPromiseComplete = false; } async _injectStyles() { try { - await Popup._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); + await this._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); } catch (e) { // NOP } @@ -426,8 +428,8 @@ class Popup { _onFullscreenChanged() { const parent = this._getFrameParentElement(); - if (parent !== null && this._container.parentNode !== parent) { - parent.appendChild(this._container); + if (parent !== null && this._frame.parentNode !== parent) { + parent.appendChild(this._frame); } } @@ -435,31 +437,31 @@ class Popup { await this._inject(); const optionsGeneral = this._options.general; - const container = this._container; - const containerRect = container.getBoundingClientRect(); - const getPosition = ( - writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ? - Popup._getPositionForHorizontalText : - Popup._getPositionForVerticalText - ); + const frame = this._frame; + const frameRect = frame.getBoundingClientRect(); - const viewport = Popup._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport); + const viewport = this._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport); const scale = this._contentScale; - const scaleRatio = this._containerSizeContentScale === null ? 1.0 : scale / this._containerSizeContentScale; - this._containerSizeContentScale = scale; - let [x, y, width, height, below] = getPosition( + const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale; + this._frameSizeContentScale = scale; + const getPositionArgs = [ elementRect, - Math.max(containerRect.width * scaleRatio, optionsGeneral.popupWidth * scale), - Math.max(containerRect.height * scaleRatio, optionsGeneral.popupHeight * scale), + Math.max(frameRect.width * scaleRatio, optionsGeneral.popupWidth * scale), + Math.max(frameRect.height * scaleRatio, optionsGeneral.popupHeight * scale), viewport, scale, optionsGeneral, writingMode + ]; + let [x, y, width, height, below] = ( + writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ? + this._getPositionForHorizontalText(...getPositionArgs) : + this._getPositionForVerticalText(...getPositionArgs) ); const fullWidth = (optionsGeneral.popupDisplayMode === 'full-width'); - container.classList.toggle('yomichan-float-full-width', fullWidth); - container.classList.toggle('yomichan-float-above', !below); + frame.classList.toggle('yomichan-float-full-width', fullWidth); + frame.classList.toggle('yomichan-float-above', !below); if (optionsGeneral.popupDisplayMode === 'full-width') { x = viewport.left; @@ -467,10 +469,10 @@ class Popup { width = viewport.right - viewport.left; } - container.style.left = `${x}px`; - container.style.top = `${y}px`; - container.style.width = `${width}px`; - container.style.height = `${height}px`; + frame.style.left = `${x}px`; + frame.style.top = `${y}px`; + frame.style.width = `${width}px`; + frame.style.height = `${height}px`; this._setVisible(true); if (this._child !== null) { @@ -484,20 +486,20 @@ class Popup { } _updateVisibility() { - this._container.style.setProperty('visibility', this.isVisibleSync() ? 'visible' : 'hidden', 'important'); + this._frame.style.setProperty('visibility', this.isVisibleSync() ? 'visible' : 'hidden', 'important'); } _focusParent() { if (this._parent !== null) { // Chrome doesn't like focusing iframe without contentWindow. - const contentWindow = this._parent._container.contentWindow; + const contentWindow = this._parent.getFrame().contentWindow; if (contentWindow !== null) { contentWindow.focus(); } } else { // Firefox doesn't like focusing window without first blurring the iframe. - // this.container.contentWindow.blur() doesn't work on Firefox for some reason. - this._container.blur(); + // this._frame.contentWindow.blur() doesn't work on Firefox for some reason. + this._frame.blur(); // This is needed for Chrome. window.focus(); } @@ -507,19 +509,19 @@ class Popup { const color = [255, 255, 255]; const {documentElement, body} = document; if (documentElement !== null) { - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(documentElement).backgroundColor)); + this._addColor(color, window.getComputedStyle(documentElement).backgroundColor); } if (body !== null) { - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(body).backgroundColor)); + this._addColor(color, window.getComputedStyle(body).backgroundColor); } const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); return dark ? 'dark' : 'light'; } _invokeApi(action, params={}) { - const secret = this._containerSecret; - const token = this._containerToken; - const contentWindow = this._container.contentWindow; + const secret = this._frameSecret; + const token = this._frameToken; + const contentWindow = this._frame.contentWindow; if (secret === null || token === null || contentWindow === null) { return; } contentWindow.postMessage({action, params, secret, token}, this._targetOrigin); @@ -541,12 +543,12 @@ class Popup { return fullscreenElement; } - static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { + _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; const verticalOffset = optionsGeneral.popupVerticalOffset * offsetScale; - const [x, w] = Popup._getConstrainedPosition( + const [x, w] = this._getConstrainedPosition( elementRect.right - horizontalOffset, elementRect.left + horizontalOffset, width, @@ -554,7 +556,7 @@ class Popup { viewport.right, true ); - const [y, h, below] = Popup._getConstrainedPositionBinary( + const [y, h, below] = this._getConstrainedPositionBinary( elementRect.top - verticalOffset, elementRect.bottom + verticalOffset, height, @@ -565,12 +567,12 @@ class Popup { return [x, y, w, h, below]; } - static _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) { - const preferRight = Popup._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); + _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) { + const preferRight = this._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); const horizontalOffset = optionsGeneral.popupHorizontalOffset2 * offsetScale; const verticalOffset = optionsGeneral.popupVerticalOffset2 * offsetScale; - const [x, w] = Popup._getConstrainedPositionBinary( + const [x, w] = this._getConstrainedPositionBinary( elementRect.left - horizontalOffset, elementRect.right + horizontalOffset, width, @@ -578,7 +580,7 @@ class Popup { viewport.right, preferRight ); - const [y, h, below] = Popup._getConstrainedPosition( + const [y, h, below] = this._getConstrainedPosition( elementRect.bottom - verticalOffset, elementRect.top + verticalOffset, height, @@ -589,20 +591,22 @@ class Popup { return [x, y, w, h, below]; } - static _isVerticalTextPopupOnRight(positionPreference, writingMode) { + _isVerticalTextPopupOnRight(positionPreference, writingMode) { switch (positionPreference) { case 'before': - return !Popup._isWritingModeLeftToRight(writingMode); + return !this._isWritingModeLeftToRight(writingMode); case 'after': - return Popup._isWritingModeLeftToRight(writingMode); + return this._isWritingModeLeftToRight(writingMode); case 'left': return false; case 'right': return true; + default: + return false; } } - static _isWritingModeLeftToRight(writingMode) { + _isWritingModeLeftToRight(writingMode) { switch (writingMode) { case 'vertical-lr': case 'sideways-lr': @@ -612,7 +616,7 @@ class Popup { } } - static _getConstrainedPosition(positionBefore, positionAfter, size, minLimit, maxLimit, after) { + _getConstrainedPosition(positionBefore, positionAfter, size, minLimit, maxLimit, after) { size = Math.min(size, maxLimit - minLimit); let position; @@ -627,7 +631,7 @@ class Popup { return [position, size, after]; } - static _getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) { + _getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) { const overflowBefore = minLimit - (positionBefore - size); const overflowAfter = (positionAfter + size) - maxLimit; @@ -647,7 +651,10 @@ class Popup { return [position, size, after]; } - static _addColor(target, color) { + _addColor(target, cssColor) { + if (typeof cssColor !== 'string') { return; } + + const color = this._getColorInfo(cssColor); if (color === null) { return; } const a = color[3]; @@ -659,7 +666,7 @@ class Popup { } } - static _getColorInfo(cssColor) { + _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; } @@ -672,7 +679,7 @@ class Popup { ]; } - static _getViewport(useVisualViewport) { + _getViewport(useVisualViewport) { const visualViewport = window.visualViewport; if (visualViewport !== null && typeof visualViewport === 'object') { const left = visualViewport.offsetLeft; @@ -706,7 +713,7 @@ class Popup { }; } - static async _injectStylesheet(id, type, value, useWebExtensionApi) { + async _injectStylesheet(id, type, value, useWebExtensionApi) { const injectedStylesheets = Popup._injectedStylesheets; if (yomichan.isExtensionUrl(window.location.href)) { -- cgit v1.2.3