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()
This commit is contained in:
toasted-nutbread 2020-05-08 19:10:06 -04:00 committed by GitHub
parent 3949db26d7
commit 48cf646973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 102 additions and 94 deletions

View File

@ -41,7 +41,7 @@ class Frontend {
this._optionsUpdatePending = false; this._optionsUpdatePending = false;
this._textScanner = new TextScanner({ this._textScanner = new TextScanner({
node: window, node: window,
ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getContainer()], ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()],
ignorePoint: (x, y) => this._popup.containsPoint(x, y), ignorePoint: (x, y) => this._popup.containsPoint(x, y),
search: this._search.bind(this) search: this._search.bind(this)
}); });

View File

@ -87,6 +87,7 @@ class PopupFactory {
popup.setParent(parent); popup.setParent(parent);
} }
this._popups.set(id, popup); this._popups.set(id, popup);
popup.prepare();
return popup; return popup;
} }
@ -168,7 +169,7 @@ class PopupFactory {
_convertPopupPointToRootPagePoint(popup, x, y) { _convertPopupPointToRootPagePoint(popup, x, y) {
if (popup.parent !== null) { if (popup.parent !== null) {
const popupRect = popup.parent.getContainerRect(); const popupRect = popup.parent.getFrameRect();
x += popupRect.x; x += popupRect.x;
y += popupRect.y; y += popupRect.y;
} }

View File

@ -36,23 +36,18 @@ class Popup {
this._options = null; this._options = null;
this._optionsContext = null; this._optionsContext = null;
this._contentScale = 1.0; this._contentScale = 1.0;
this._containerSizeContentScale = null;
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
this._previousOptionsContextSource = null; this._previousOptionsContextSource = null;
this._containerSecret = null;
this._containerToken = null;
this._container = document.createElement('iframe'); this._frameSizeContentScale = null;
this._container.className = 'yomichan-float'; this._frameSecret = null;
this._container.addEventListener('mousedown', (e) => e.stopPropagation()); this._frameToken = null;
this._container.addEventListener('scroll', (e) => e.stopPropagation()); this._frame = document.createElement('iframe');
this._container.style.width = '0px'; this._frame.className = 'yomichan-float';
this._container.style.height = '0px'; this._frame.style.width = '0';
this._container.addEventListener('load', this._onFrameLoad.bind(this)); this._frame.style.height = '0';
this._fullscreenEventListeners = new EventListenerCollection(); this._fullscreenEventListeners = new EventListenerCollection();
this._updateVisibility();
} }
// Public properties // Public properties
@ -79,6 +74,13 @@ class Popup {
// Public functions // 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() { isProxy() {
return false; return false;
} }
@ -118,7 +120,7 @@ class Popup {
async containsPoint(x, y) { async containsPoint(x, y) {
for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup._child) { 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) { if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) {
return true; return true;
} }
@ -173,12 +175,12 @@ class Popup {
} }
updateTheme() { updateTheme() {
this._container.dataset.yomichanTheme = this._options.general.popupOuterTheme; this._frame.dataset.yomichanTheme = this._options.general.popupOuterTheme;
this._container.dataset.yomichanSiteColor = this._getSiteColor(); this._frame.dataset.yomichanSiteColor = this._getSiteColor();
} }
async setCustomOuterCss(css, useWebExtensionApi) { async setCustomOuterCss(css, useWebExtensionApi) {
return await Popup._injectStylesheet( return await this._injectStylesheet(
'yomichan-popup-outer-user-stylesheet', 'yomichan-popup-outer-user-stylesheet',
'code', 'code',
css, css,
@ -190,12 +192,12 @@ class Popup {
this._childrenSupported = value; this._childrenSupported = value;
} }
getContainer() { getFrame() {
return this._container; return this._frame;
} }
getContainerRect() { getFrameRect() {
return this._container.getBoundingClientRect(); return this._frame.getBoundingClientRect();
} }
// Private functions // Private functions
@ -220,11 +222,11 @@ class Popup {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tokenMap = new Map(); const tokenMap = new Map();
let timer = null; let timer = null;
let containerLoadedResolve = null; let frameLoadedResolve = null;
let containerLoadedReject = null; let frameLoadedReject = null;
const containerLoaded = new Promise((resolve2, reject2) => { const frameLoaded = new Promise((resolve2, reject2) => {
containerLoadedResolve = resolve2; frameLoadedResolve = resolve2;
containerLoadedReject = reject2; frameLoadedReject = reject2;
}); });
const postMessage = (action, params) => { const postMessage = (action, params) => {
@ -252,7 +254,7 @@ class Popup {
if (!isObject(message)) { return; } if (!isObject(message)) { return; }
const {action, params} = message; const {action, params} = message;
if (!isObject(params)) { return; } if (!isObject(params)) { return; }
await containerLoaded; await frameLoaded;
if (timer === null) { return; } // Done if (timer === null) { return; } // Done
switch (action) { switch (action) {
@ -282,7 +284,7 @@ class Popup {
}; };
const onLoad = () => { const onLoad = () => {
if (containerLoadedResolve === null) { if (frameLoadedResolve === null) {
cleanup(); cleanup();
reject(new Error('Unexpected load event')); reject(new Error('Unexpected load event'));
return; return;
@ -292,9 +294,9 @@ class Popup {
return; return;
} }
containerLoadedResolve(); frameLoadedResolve();
containerLoadedResolve = null; frameLoadedResolve = null;
containerLoadedReject = null; frameLoadedReject = null;
}; };
const cleanup = () => { const cleanup = () => {
@ -302,10 +304,10 @@ class Popup {
clearTimeout(timer); clearTimeout(timer);
timer = null; timer = null;
containerLoadedResolve = null; frameLoadedResolve = null;
if (containerLoadedReject !== null) { if (frameLoadedReject !== null) {
containerLoadedReject(new Error('Terminated')); frameLoadedReject(new Error('Terminated'));
containerLoadedReject = null; frameLoadedReject = null;
} }
chrome.runtime.onMessage.removeListener(onMessage); chrome.runtime.onMessage.removeListener(onMessage);
@ -322,7 +324,7 @@ class Popup {
frame.addEventListener('load', onLoad); frame.addEventListener('load', onLoad);
// Prevent unhandled rejections // Prevent unhandled rejections
containerLoaded.catch(() => {}); // NOP frameLoaded.catch(() => {}); // NOP
setupFrame(frame); setupFrame(frame);
}); });
@ -331,15 +333,15 @@ class Popup {
async _createInjectPromise() { async _createInjectPromise() {
this._injectStyles(); 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('src');
frame.removeAttribute('srcdoc'); frame.removeAttribute('srcdoc');
frame.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); frame.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
this._observeFullscreen(true); this._observeFullscreen(true);
this._onFullscreenChanged(); this._onFullscreenChanged();
}); });
this._containerSecret = secret; this._frameSecret = secret;
this._containerToken = token; this._frameToken = token;
// Configure // Configure
const messageId = yomichan.generateId(16); const messageId = yomichan.generateId(16);
@ -374,22 +376,22 @@ class Popup {
} }
_resetFrame() { _resetFrame() {
const parent = this._container.parentNode; const parent = this._frame.parentNode;
if (parent !== null) { if (parent !== null) {
parent.removeChild(this._container); parent.removeChild(this._frame);
} }
this._container.removeAttribute('src'); this._frame.removeAttribute('src');
this._container.removeAttribute('srcdoc'); this._frame.removeAttribute('srcdoc');
this._containerSecret = null; this._frameSecret = null;
this._containerToken = null; this._frameToken = null;
this._injectPromise = null; this._injectPromise = null;
this._injectPromiseComplete = false; this._injectPromiseComplete = false;
} }
async _injectStyles() { async _injectStyles() {
try { 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) { } catch (e) {
// NOP // NOP
} }
@ -426,8 +428,8 @@ class Popup {
_onFullscreenChanged() { _onFullscreenChanged() {
const parent = this._getFrameParentElement(); const parent = this._getFrameParentElement();
if (parent !== null && this._container.parentNode !== parent) { if (parent !== null && this._frame.parentNode !== parent) {
parent.appendChild(this._container); parent.appendChild(this._frame);
} }
} }
@ -435,31 +437,31 @@ class Popup {
await this._inject(); await this._inject();
const optionsGeneral = this._options.general; const optionsGeneral = this._options.general;
const container = this._container; const frame = this._frame;
const containerRect = container.getBoundingClientRect(); const frameRect = frame.getBoundingClientRect();
const getPosition = (
writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ?
Popup._getPositionForHorizontalText :
Popup._getPositionForVerticalText
);
const viewport = Popup._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport); const viewport = this._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport);
const scale = this._contentScale; const scale = this._contentScale;
const scaleRatio = this._containerSizeContentScale === null ? 1.0 : scale / this._containerSizeContentScale; const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale;
this._containerSizeContentScale = scale; this._frameSizeContentScale = scale;
let [x, y, width, height, below] = getPosition( const getPositionArgs = [
elementRect, elementRect,
Math.max(containerRect.width * scaleRatio, optionsGeneral.popupWidth * scale), Math.max(frameRect.width * scaleRatio, optionsGeneral.popupWidth * scale),
Math.max(containerRect.height * scaleRatio, optionsGeneral.popupHeight * scale), Math.max(frameRect.height * scaleRatio, optionsGeneral.popupHeight * scale),
viewport, viewport,
scale, scale,
optionsGeneral, optionsGeneral,
writingMode 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'); const fullWidth = (optionsGeneral.popupDisplayMode === 'full-width');
container.classList.toggle('yomichan-float-full-width', fullWidth); frame.classList.toggle('yomichan-float-full-width', fullWidth);
container.classList.toggle('yomichan-float-above', !below); frame.classList.toggle('yomichan-float-above', !below);
if (optionsGeneral.popupDisplayMode === 'full-width') { if (optionsGeneral.popupDisplayMode === 'full-width') {
x = viewport.left; x = viewport.left;
@ -467,10 +469,10 @@ class Popup {
width = viewport.right - viewport.left; width = viewport.right - viewport.left;
} }
container.style.left = `${x}px`; frame.style.left = `${x}px`;
container.style.top = `${y}px`; frame.style.top = `${y}px`;
container.style.width = `${width}px`; frame.style.width = `${width}px`;
container.style.height = `${height}px`; frame.style.height = `${height}px`;
this._setVisible(true); this._setVisible(true);
if (this._child !== null) { if (this._child !== null) {
@ -484,20 +486,20 @@ class Popup {
} }
_updateVisibility() { _updateVisibility() {
this._container.style.setProperty('visibility', this.isVisibleSync() ? 'visible' : 'hidden', 'important'); this._frame.style.setProperty('visibility', this.isVisibleSync() ? 'visible' : 'hidden', 'important');
} }
_focusParent() { _focusParent() {
if (this._parent !== null) { if (this._parent !== null) {
// Chrome doesn't like focusing iframe without contentWindow. // Chrome doesn't like focusing iframe without contentWindow.
const contentWindow = this._parent._container.contentWindow; const contentWindow = this._parent.getFrame().contentWindow;
if (contentWindow !== null) { if (contentWindow !== null) {
contentWindow.focus(); contentWindow.focus();
} }
} else { } else {
// Firefox doesn't like focusing window without first blurring the iframe. // Firefox doesn't like focusing window without first blurring the iframe.
// this.container.contentWindow.blur() doesn't work on Firefox for some reason. // this._frame.contentWindow.blur() doesn't work on Firefox for some reason.
this._container.blur(); this._frame.blur();
// This is needed for Chrome. // This is needed for Chrome.
window.focus(); window.focus();
} }
@ -507,19 +509,19 @@ class Popup {
const color = [255, 255, 255]; const color = [255, 255, 255];
const {documentElement, body} = document; const {documentElement, body} = document;
if (documentElement !== null) { if (documentElement !== null) {
Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(documentElement).backgroundColor)); this._addColor(color, window.getComputedStyle(documentElement).backgroundColor);
} }
if (body !== null) { 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); const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128);
return dark ? 'dark' : 'light'; return dark ? 'dark' : 'light';
} }
_invokeApi(action, params={}) { _invokeApi(action, params={}) {
const secret = this._containerSecret; const secret = this._frameSecret;
const token = this._containerToken; const token = this._frameToken;
const contentWindow = this._container.contentWindow; const contentWindow = this._frame.contentWindow;
if (secret === null || token === null || contentWindow === null) { return; } if (secret === null || token === null || contentWindow === null) { return; }
contentWindow.postMessage({action, params, secret, token}, this._targetOrigin); contentWindow.postMessage({action, params, secret, token}, this._targetOrigin);
@ -541,12 +543,12 @@ class Popup {
return fullscreenElement; return fullscreenElement;
} }
static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {
const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below');
const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale;
const verticalOffset = optionsGeneral.popupVerticalOffset * offsetScale; const verticalOffset = optionsGeneral.popupVerticalOffset * offsetScale;
const [x, w] = Popup._getConstrainedPosition( const [x, w] = this._getConstrainedPosition(
elementRect.right - horizontalOffset, elementRect.right - horizontalOffset,
elementRect.left + horizontalOffset, elementRect.left + horizontalOffset,
width, width,
@ -554,7 +556,7 @@ class Popup {
viewport.right, viewport.right,
true true
); );
const [y, h, below] = Popup._getConstrainedPositionBinary( const [y, h, below] = this._getConstrainedPositionBinary(
elementRect.top - verticalOffset, elementRect.top - verticalOffset,
elementRect.bottom + verticalOffset, elementRect.bottom + verticalOffset,
height, height,
@ -565,12 +567,12 @@ class Popup {
return [x, y, w, h, below]; return [x, y, w, h, below];
} }
static _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) { _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) {
const preferRight = Popup._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); const preferRight = this._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode);
const horizontalOffset = optionsGeneral.popupHorizontalOffset2 * offsetScale; const horizontalOffset = optionsGeneral.popupHorizontalOffset2 * offsetScale;
const verticalOffset = optionsGeneral.popupVerticalOffset2 * offsetScale; const verticalOffset = optionsGeneral.popupVerticalOffset2 * offsetScale;
const [x, w] = Popup._getConstrainedPositionBinary( const [x, w] = this._getConstrainedPositionBinary(
elementRect.left - horizontalOffset, elementRect.left - horizontalOffset,
elementRect.right + horizontalOffset, elementRect.right + horizontalOffset,
width, width,
@ -578,7 +580,7 @@ class Popup {
viewport.right, viewport.right,
preferRight preferRight
); );
const [y, h, below] = Popup._getConstrainedPosition( const [y, h, below] = this._getConstrainedPosition(
elementRect.bottom - verticalOffset, elementRect.bottom - verticalOffset,
elementRect.top + verticalOffset, elementRect.top + verticalOffset,
height, height,
@ -589,20 +591,22 @@ class Popup {
return [x, y, w, h, below]; return [x, y, w, h, below];
} }
static _isVerticalTextPopupOnRight(positionPreference, writingMode) { _isVerticalTextPopupOnRight(positionPreference, writingMode) {
switch (positionPreference) { switch (positionPreference) {
case 'before': case 'before':
return !Popup._isWritingModeLeftToRight(writingMode); return !this._isWritingModeLeftToRight(writingMode);
case 'after': case 'after':
return Popup._isWritingModeLeftToRight(writingMode); return this._isWritingModeLeftToRight(writingMode);
case 'left': case 'left':
return false; return false;
case 'right': case 'right':
return true; return true;
default:
return false;
} }
} }
static _isWritingModeLeftToRight(writingMode) { _isWritingModeLeftToRight(writingMode) {
switch (writingMode) { switch (writingMode) {
case 'vertical-lr': case 'vertical-lr':
case 'sideways-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); size = Math.min(size, maxLimit - minLimit);
let position; let position;
@ -627,7 +631,7 @@ class Popup {
return [position, size, after]; 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 overflowBefore = minLimit - (positionBefore - size);
const overflowAfter = (positionAfter + size) - maxLimit; const overflowAfter = (positionAfter + size) - maxLimit;
@ -647,7 +651,10 @@ class Popup {
return [position, size, after]; 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; } if (color === null) { return; }
const a = color[3]; 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); 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; } if (m === null) { return null; }
@ -672,7 +679,7 @@ class Popup {
]; ];
} }
static _getViewport(useVisualViewport) { _getViewport(useVisualViewport) {
const visualViewport = window.visualViewport; const visualViewport = window.visualViewport;
if (visualViewport !== null && typeof visualViewport === 'object') { if (visualViewport !== null && typeof visualViewport === 'object') {
const left = visualViewport.offsetLeft; 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; const injectedStylesheets = Popup._injectedStylesheets;
if (yomichan.isExtensionUrl(window.location.href)) { if (yomichan.isExtensionUrl(window.location.href)) {