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)) {