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._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)
});

View File

@ -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;
}

View File

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