summaryrefslogtreecommitdiff
path: root/ext/fg/js/popup.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg/js/popup.js')
-rw-r--r--ext/fg/js/popup.js687
1 files changed, 0 insertions, 687 deletions
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
deleted file mode 100644
index 75b74257..00000000
--- a/ext/fg/js/popup.js
+++ /dev/null
@@ -1,687 +0,0 @@
-/*
- * Copyright (C) 2016-2021 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 <https://www.gnu.org/licenses/>.
- */
-
-/* global
- * DocumentUtil
- * FrameClient
- * api
- * dynamicLoader
- */
-
-class Popup extends EventDispatcher {
- constructor({
- id,
- depth,
- frameId,
- childrenSupported
- }) {
- super();
- this._id = id;
- this._depth = depth;
- this._frameId = frameId;
- this._childrenSupported = childrenSupported;
- this._parent = null;
- this._child = null;
- this._injectPromise = null;
- this._injectPromiseComplete = false;
- this._visible = new DynamicProperty(false);
- this._options = null;
- this._optionsContext = null;
- this._contentScale = 1.0;
- this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
-
- this._frameSizeContentScale = null;
- this._frameClient = null;
- this._frame = document.createElement('iframe');
- this._frame.className = 'yomichan-popup';
- this._frame.style.width = '0';
- this._frame.style.height = '0';
-
- this._container = this._frame;
- this._shadow = null;
-
- this._fullscreenEventListeners = new EventListenerCollection();
- }
-
- // Public properties
-
- get id() {
- return this._id;
- }
-
- get parent() {
- return this._parent;
- }
-
- set parent(value) {
- this._parent = value;
- }
-
- get child() {
- return this._child;
- }
-
- set child(value) {
- this._child = value;
- }
-
- get depth() {
- return this._depth;
- }
-
- get frameContentWindow() {
- return this._frame.contentWindow;
- }
-
- get container() {
- return this._container;
- }
-
- get frameId() {
- return this._frameId;
- }
-
- // Public functions
-
- prepare() {
- this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this));
- this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this));
- this._frame.addEventListener('mousedown', (e) => e.stopPropagation());
- this._frame.addEventListener('scroll', (e) => e.stopPropagation());
- this._frame.addEventListener('load', this._onFrameLoad.bind(this));
- this._visible.on('change', this._onVisibleChange.bind(this));
- yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));
- this._onVisibleChange({value: this.isVisibleSync()});
- }
-
- async setOptionsContext(optionsContext) {
- await this._setOptionsContext(optionsContext);
- await this._invokeSafe('setOptionsContext', {optionsContext});
- }
-
- hide(changeFocus) {
- if (!this.isVisibleSync()) {
- return;
- }
-
- this._setVisible(false);
- if (this._child !== null) {
- this._child.hide(false);
- }
- if (changeFocus) {
- this._focusParent();
- }
- }
-
- async isVisible() {
- return this.isVisibleSync();
- }
-
- async setVisibleOverride(value, priority) {
- return this._visible.setOverride(value, priority);
- }
-
- async clearVisibleOverride(token) {
- return this._visible.clearOverride(token);
- }
-
- async containsPoint(x, y) {
- for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) {
- const rect = popup.getFrameRect();
- if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) {
- return true;
- }
- }
- return false;
- }
-
- async showContent(details, displayDetails) {
- if (this._options === null) { throw new Error('Options not assigned'); }
-
- const {optionsContext, elementRect, writingMode} = details;
- if (optionsContext !== null) {
- await this._setOptionsContextIfDifferent(optionsContext);
- }
-
- if (typeof elementRect !== 'undefined' && typeof writingMode !== 'undefined') {
- await this._show(elementRect, writingMode);
- }
-
- if (displayDetails !== null) {
- this._invokeSafe('setContent', {details: displayDetails});
- }
- }
-
- setCustomCss(css) {
- this._invokeSafe('setCustomCss', {css});
- }
-
- clearAutoPlayTimer() {
- this._invokeSafe('clearAutoPlayTimer');
- }
-
- setContentScale(scale) {
- this._contentScale = scale;
- this._frame.style.fontSize = `${scale}px`;
- this._invokeSafe('setContentScale', {scale});
- }
-
- isVisibleSync() {
- return this._visible.value;
- }
-
- updateTheme() {
- const {popupTheme, popupOuterTheme} = this._options.general;
- this._frame.dataset.theme = popupTheme;
- this._frame.dataset.outerTheme = popupOuterTheme;
- this._frame.dataset.siteColor = this._getSiteColor();
- }
-
- async setCustomOuterCss(css, useWebExtensionApi) {
- let parentNode = null;
- const inShadow = (this._shadow !== null);
- if (inShadow) {
- useWebExtensionApi = false;
- parentNode = this._shadow;
- }
- const node = await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode);
- this.trigger('customOuterCssChanged', {node, useWebExtensionApi, inShadow});
- }
-
- getFrameRect() {
- return this._frame.getBoundingClientRect();
- }
-
- async getFrameSize() {
- const rect = this._frame.getBoundingClientRect();
- return {width: rect.width, height: rect.height, valid: true};
- }
-
- async setFrameSize(width, height) {
- this._setFrameSize(width, height);
- return true;
- }
-
- // Private functions
-
- _onFrameMouseOver() {
- this.trigger('framePointerOver', {});
- }
-
- _onFrameMouseOut() {
- this.trigger('framePointerOut', {});
- }
-
- _inject() {
- let injectPromise = this._injectPromise;
- if (injectPromise === null) {
- injectPromise = this._createInjectPromise();
- this._injectPromise = injectPromise;
- injectPromise.then(
- () => {
- if (injectPromise !== this._injectPromise) { return; }
- this._injectPromiseComplete = true;
- },
- () => { this._resetFrame(); }
- );
- }
- return injectPromise;
- }
-
- async _createInjectPromise() {
- if (this._options === null) {
- throw new Error('Options not initialized');
- }
-
- const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general;
-
- await this._setUpContainer(usePopupShadowDom);
-
- const setupFrame = (frame) => {
- frame.removeAttribute('src');
- frame.removeAttribute('srcdoc');
- this._observeFullscreen(true);
- this._onFullscreenChanged();
- const url = chrome.runtime.getURL('/popup.html');
- if (useSecurePopupFrameUrl) {
- frame.contentDocument.location.href = url;
- } else {
- frame.setAttribute('src', url);
- }
- };
-
- const frameClient = new FrameClient();
- this._frameClient = frameClient;
- await frameClient.connect(this._frame, this._targetOrigin, this._frameId, setupFrame);
-
- // Configure
- await this._invokeSafe('configure', {
- depth: this._depth,
- parentPopupId: this._id,
- parentFrameId: this._frameId,
- childrenSupported: this._childrenSupported,
- scale: this._contentScale,
- optionsContext: this._optionsContext
- });
- }
-
- _onFrameLoad() {
- if (!this._injectPromiseComplete) { return; }
- this._resetFrame();
- }
-
- _resetFrame() {
- const parent = this._container.parentNode;
- if (parent !== null) {
- parent.removeChild(this._container);
- }
- this._frame.removeAttribute('src');
- this._frame.removeAttribute('srcdoc');
-
- this._frameClient = null;
- this._injectPromise = null;
- this._injectPromiseComplete = false;
- }
-
- async _setUpContainer(usePopupShadowDom) {
- if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') {
- const container = document.createElement('div');
- container.style.setProperty('all', 'initial', 'important');
- const shadow = container.attachShadow({mode: 'closed', delegatesFocus: true});
- shadow.appendChild(this._frame);
-
- this._container = container;
- this._shadow = shadow;
- } else {
- const frameParentNode = this._frame.parentNode;
- if (frameParentNode !== null) {
- frameParentNode.removeChild(this._frame);
- }
-
- this._container = this._frame;
- this._shadow = null;
- }
-
- await this._injectStyles();
- }
-
- async _injectStyles() {
- try {
- await this._injectPopupOuterStylesheet();
- } catch (e) {
- // NOP
- }
-
- try {
- await this.setCustomOuterCss(this._options.general.customPopupOuterCss, true);
- } catch (e) {
- // NOP
- }
- }
-
- async _injectPopupOuterStylesheet() {
- let fileType = 'file';
- let useWebExtensionApi = true;
- let parentNode = null;
- if (this._shadow !== null) {
- fileType = 'file-content';
- useWebExtensionApi = false;
- parentNode = this._shadow;
- }
- await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', fileType, '/css/popup-outer.css', useWebExtensionApi, parentNode);
- }
-
- _observeFullscreen(observe) {
- if (!observe) {
- this._fullscreenEventListeners.removeAllEventListeners();
- return;
- }
-
- if (this._fullscreenEventListeners.size > 0) {
- // Already observing
- return;
- }
-
- DocumentUtil.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners);
- }
-
- _onFullscreenChanged() {
- const parent = this._getFrameParentElement();
- if (parent !== null && this._container.parentNode !== parent) {
- parent.appendChild(this._container);
- }
- }
-
- async _show(elementRect, writingMode) {
- await this._inject();
-
- const optionsGeneral = this._options.general;
- const {popupDisplayMode} = optionsGeneral;
- const frame = this._frame;
- const frameRect = frame.getBoundingClientRect();
-
- const viewport = this._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport);
- const scale = this._contentScale;
- const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale;
- this._frameSizeContentScale = scale;
- const getPositionArgs = [
- elementRect,
- 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)
- );
-
- frame.dataset.popupDisplayMode = popupDisplayMode;
- frame.dataset.below = `${below}`;
-
- if (popupDisplayMode === 'full-width') {
- x = viewport.left;
- y = below ? viewport.bottom - height : viewport.top;
- width = viewport.right - viewport.left;
- }
-
- frame.style.left = `${x}px`;
- frame.style.top = `${y}px`;
- this._setFrameSize(width, height);
-
- this._setVisible(true);
- if (this._child !== null) {
- this._child.hide(true);
- }
- }
-
- _setFrameSize(width, height) {
- const {style} = this._frame;
- style.width = `${width}px`;
- style.height = `${height}px`;
- }
-
- _setVisible(visible) {
- this._visible.defaultValue = visible;
- }
-
- _onVisibleChange({value}) {
- this._frame.style.setProperty('visibility', value ? 'visible' : 'hidden', 'important');
- }
-
- _focusParent() {
- if (this._parent !== null) {
- // Chrome doesn't like focusing iframe without contentWindow.
- const contentWindow = this._parent.frameContentWindow;
- if (contentWindow !== null) {
- contentWindow.focus();
- }
- } else {
- // Firefox doesn't like focusing window without first blurring the iframe.
- // this._frame.contentWindow.blur() doesn't work on Firefox for some reason.
- this._frame.blur();
- // This is needed for Chrome.
- window.focus();
- }
- }
-
- _getSiteColor() {
- const color = [255, 255, 255];
- const {documentElement, body} = document;
- if (documentElement !== null) {
- this._addColor(color, window.getComputedStyle(documentElement).backgroundColor);
- }
- if (body !== null) {
- this._addColor(color, window.getComputedStyle(body).backgroundColor);
- }
- const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128);
- return dark ? 'dark' : 'light';
- }
-
- async _invoke(action, params={}) {
- const contentWindow = this._frame.contentWindow;
- if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; }
-
- const message = this._frameClient.createMessage({action, params});
- return await api.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message);
- }
-
- async _invokeSafe(action, params={}, defaultReturnValue) {
- try {
- return await this._invoke(action, params);
- } catch (e) {
- if (!yomichan.isExtensionUnloaded) { throw e; }
- return defaultReturnValue;
- }
- }
-
- _invokeWindow(action, params={}) {
- const contentWindow = this._frame.contentWindow;
- if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; }
-
- const message = this._frameClient.createMessage({action, params});
- contentWindow.postMessage(message, this._targetOrigin);
- }
-
- _onExtensionUnloaded() {
- this._invokeWindow('extensionUnloaded');
- }
-
- _getFrameParentElement() {
- const defaultParent = document.body;
- const fullscreenElement = DocumentUtil.getFullscreenElement();
- if (
- fullscreenElement === null ||
- fullscreenElement.shadowRoot ||
- fullscreenElement.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions
- ) {
- return defaultParent;
- }
-
- switch (fullscreenElement.nodeName.toUpperCase()) {
- case 'IFRAME':
- case 'FRAME':
- return defaultParent;
- }
-
- return fullscreenElement;
- }
-
- _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] = this._getConstrainedPosition(
- elementRect.right - horizontalOffset,
- elementRect.left + horizontalOffset,
- width,
- viewport.left,
- viewport.right,
- true
- );
- const [y, h, below] = this._getConstrainedPositionBinary(
- elementRect.top - verticalOffset,
- elementRect.bottom + verticalOffset,
- height,
- viewport.top,
- viewport.bottom,
- preferBelow
- );
- return [x, y, w, h, below];
- }
-
- _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] = this._getConstrainedPositionBinary(
- elementRect.left - horizontalOffset,
- elementRect.right + horizontalOffset,
- width,
- viewport.left,
- viewport.right,
- preferRight
- );
- const [y, h, below] = this._getConstrainedPosition(
- elementRect.bottom - verticalOffset,
- elementRect.top + verticalOffset,
- height,
- viewport.top,
- viewport.bottom,
- true
- );
- return [x, y, w, h, below];
- }
-
- _isVerticalTextPopupOnRight(positionPreference, writingMode) {
- switch (positionPreference) {
- case 'before':
- return !this._isWritingModeLeftToRight(writingMode);
- case 'after':
- return this._isWritingModeLeftToRight(writingMode);
- case 'left':
- return false;
- case 'right':
- return true;
- default:
- return false;
- }
- }
-
- _isWritingModeLeftToRight(writingMode) {
- switch (writingMode) {
- case 'vertical-lr':
- case 'sideways-lr':
- return true;
- default:
- return false;
- }
- }
-
- _getConstrainedPosition(positionBefore, positionAfter, size, minLimit, maxLimit, after) {
- size = Math.min(size, maxLimit - minLimit);
-
- let position;
- if (after) {
- position = Math.max(minLimit, positionAfter);
- position = position - Math.max(0, (position + size) - maxLimit);
- } else {
- position = Math.min(maxLimit, positionBefore) - size;
- position = position + Math.max(0, minLimit - position);
- }
-
- return [position, size, after];
- }
-
- _getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) {
- const overflowBefore = minLimit - (positionBefore - size);
- const overflowAfter = (positionAfter + size) - maxLimit;
-
- if (overflowAfter > 0 || overflowBefore > 0) {
- after = (overflowAfter < overflowBefore);
- }
-
- let position;
- if (after) {
- size -= Math.max(0, overflowAfter);
- position = Math.max(minLimit, positionAfter);
- } else {
- size -= Math.max(0, overflowBefore);
- position = Math.min(maxLimit, positionBefore) - size;
- }
-
- return [position, size, after];
- }
-
- _addColor(target, cssColor) {
- if (typeof cssColor !== 'string') { return; }
-
- const color = this._getColorInfo(cssColor);
- if (color === null) { return; }
-
- const a = color[3];
- if (a <= 0.0) { return; }
-
- const aInv = 1.0 - a;
- for (let i = 0; i < 3; ++i) {
- target[i] = target[i] * aInv + color[i] * a;
- }
- }
-
- _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; }
-
- const m4 = m[4];
- return [
- Number.parseInt(m[1], 10),
- Number.parseInt(m[2], 10),
- Number.parseInt(m[3], 10),
- m4 ? Math.max(0.0, Math.min(1.0, Number.parseFloat(m4))) : 1.0
- ];
- }
-
- _getViewport(useVisualViewport) {
- const visualViewport = window.visualViewport;
- if (visualViewport !== null && typeof visualViewport === 'object') {
- const left = visualViewport.offsetLeft;
- const top = visualViewport.offsetTop;
- const width = visualViewport.width;
- const height = visualViewport.height;
- if (useVisualViewport) {
- return {
- left,
- top,
- right: left + width,
- bottom: top + height
- };
- } else {
- const scale = visualViewport.scale;
- return {
- left: 0,
- top: 0,
- right: Math.max(left + width, width * scale),
- bottom: Math.max(top + height, height * scale)
- };
- }
- }
-
- const body = document.body;
- return {
- left: 0,
- top: 0,
- right: (body !== null ? body.clientWidth : 0),
- bottom: window.innerHeight
- };
- }
-
- async _setOptionsContext(optionsContext) {
- this._optionsContext = optionsContext;
- this._options = await api.optionsGet(optionsContext);
- this.updateTheme();
- }
-
- async _setOptionsContextIfDifferent(optionsContext) {
- if (deepEqual(this._optionsContext, optionsContext)) { return; }
- await this._setOptionsContext(optionsContext);
- }
-}