diff options
Diffstat (limited to 'ext/fg/js/popup.js')
| -rw-r--r-- | ext/fg/js/popup.js | 524 | 
1 files changed, 286 insertions, 238 deletions
| diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 42475d96..7a0c6133 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2016-2017  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,100 +13,241 @@   * 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 <http://www.gnu.org/licenses/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */  class Popup {      constructor(id, depth, frameIdPromise) { -        this.id = id; -        this.depth = depth; -        this.frameIdPromise = frameIdPromise; -        this.frameId = null; -        this.parent = null; -        this.child = null; -        this.childrenSupported = true; -        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.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); -        this.container.style.width = '0px'; -        this.container.style.height = '0px'; -        this.injectPromise = null; -        this.isInjected = false; -        this.visible = false; -        this.visibleOverride = null; -        this.options = null; -        this.stylesheetInjectedViaApi = false; -        this.updateVisibility(); -    } - -    inject() { -        if (this.injectPromise === null) { -            this.injectPromise = this.createInjectPromise(); +        this._id = id; +        this._depth = depth; +        this._frameIdPromise = frameIdPromise; +        this._frameId = null; +        this._parent = null; +        this._child = null; +        this._childrenSupported = true; +        this._injectPromise = null; +        this._isInjected = false; +        this._visible = false; +        this._visibleOverride = null; +        this._options = null; +        this._stylesheetInjectedViaApi = false; + +        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.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); +        this._container.style.width = '0px'; +        this._container.style.height = '0px'; + +        this._updateVisibility(); +    } + +    // Public properties + +    get parent() { +        return this._parent; +    } + +    get depth() { +        return this._depth; +    } + +    get url() { +        return window.location.href; +    } + +    // Public functions + +    isProxy() { +        return false; +    } + +    async setOptions(options) { +        this._options = options; +        this.updateTheme(); +    } + +    hide(changeFocus) { +        if (!this.isVisibleSync()) { +            return; +        } + +        this._setVisible(false); +        if (this._child !== null) { +            this._child.hide(false); +        } +        if (changeFocus) { +            this._focusParent();          } -        return this.injectPromise;      } -    async createInjectPromise() { +    async isVisible() { +        return this.isVisibleSync(); +    } + +    setVisibleOverride(visible) { +        this._visibleOverride = visible; +        this._updateVisibility(); +    } + +    async containsPoint(x, y) { +        for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup._child) { +            const rect = popup._container.getBoundingClientRect(); +            if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { +                return true; +            } +        } +        return false; +    } + +    async showContent(elementRect, writingMode, type=null, details=null) { +        if (!this._isInitialized()) { return; } +        await this._show(elementRect, writingMode); +        if (type === null) { return; } +        this._invokeApi('setContent', {type, details}); +    } + +    async setCustomCss(css) { +        this._invokeApi('setCustomCss', {css}); +    } + +    clearAutoPlayTimer() { +        if (this._isInjected) { +            this._invokeApi('clearAutoPlayTimer'); +        } +    } + +    // Popup-only public functions + +    setParent(parent) { +        if (parent === null) { +            throw new Error('Cannot set popup parent to null'); +        } +        if (this._parent !== null) { +            throw new Error('Popup already has a parent'); +        } +        if (parent._child !== null) { +            throw new Error('Cannot parent popup to another popup which already has a child'); +        } +        this._parent = parent; +        parent._child = this; +    } + +    isVisibleSync() { +        return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible); +    } + +    updateTheme() { +        this._container.dataset.yomichanTheme = this._options.general.popupOuterTheme; +        this._container.dataset.yomichanSiteColor = this._getSiteColor(); +    } + +    async setCustomOuterCss(css, injectDirectly) { +        // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. +        if (this._stylesheetInjectedViaApi) { return; } + +        if (injectDirectly || Popup._isOnExtensionPage()) { +            Popup.injectOuterStylesheet(css); +        } else { +            if (!css) { return; } +            try { +                await apiInjectStylesheet(css); +                this._stylesheetInjectedViaApi = true; +            } catch (e) { +                // NOP +            } +        } +    } + +    setChildrenSupported(value) { +        this._childrenSupported = value; +    } + +    getContainer() { +        return this._container; +    } + +    getContainerRect() { +        return this._container.getBoundingClientRect(); +    } + +    static injectOuterStylesheet(css) { +        if (Popup.outerStylesheet === null) { +            if (!css) { return; } +            Popup.outerStylesheet = document.createElement('style'); +            Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet'; +        } + +        const outerStylesheet = Popup.outerStylesheet; +        if (css) { +            outerStylesheet.textContent = css; + +            const par = document.head; +            if (par && outerStylesheet.parentNode !== par) { +                par.appendChild(outerStylesheet); +            } +        } else { +            outerStylesheet.textContent = ''; +        } +    } + +    // Private functions + +    _inject() { +        if (this._injectPromise === null) { +            this._injectPromise = this._createInjectPromise(); +        } +        return this._injectPromise; +    } + +    async _createInjectPromise() {          try { -            const {frameId} = await this.frameIdPromise; +            const {frameId} = await this._frameIdPromise;              if (typeof frameId === 'number') { -                this.frameId = frameId; +                this._frameId = frameId;              }          } catch (e) {              // NOP          }          return new Promise((resolve) => { -            const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null); -            this.container.addEventListener('load', () => { -                this.invokeApi('initialize', { -                    options: this.options, +            const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); +            this._container.addEventListener('load', () => { +                this._invokeApi('initialize', { +                    options: this._options,                      popupInfo: { -                        id: this.id, -                        depth: this.depth, +                        id: this._id, +                        depth: this._depth,                          parentFrameId                      },                      url: this.url, -                    childrenSupported: this.childrenSupported +                    childrenSupported: this._childrenSupported                  });                  resolve();              }); -            this.observeFullscreen(); -            this.onFullscreenChanged(); -            this.setCustomOuterCss(this.options.general.customPopupOuterCss, false); -            this.isInjected = true; +            this._observeFullscreen(); +            this._onFullscreenChanged(); +            this.setCustomOuterCss(this._options.general.customPopupOuterCss, false); +            this._isInjected = true;          });      } -    isInitialized() { -        return this.options !== null; +    _isInitialized() { +        return this._options !== null;      } -    async setOptions(options) { -        this.options = options; -        this.updateTheme(); -    } +    async _show(elementRect, writingMode) { +        await this._inject(); -    async showContent(elementRect, writingMode, type=null, details=null) { -        if (!this.isInitialized()) { return; } -        await this.show(elementRect, writingMode); -        if (type === null) { return; } -        this.invokeApi('setContent', {type, details}); -    } - -    async show(elementRect, writingMode) { -        await this.inject(); - -        const optionsGeneral = this.options.general; -        const container = this.container; +        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 +            Popup._getPositionForHorizontalText : +            Popup._getPositionForVerticalText          );          const [x, y, width, height, below] = getPosition( @@ -126,13 +267,78 @@ class Popup {          container.style.width = `${width}px`;          container.style.height = `${height}px`; -        this.setVisible(true); -        if (this.child !== null) { -            this.child.hide(true); +        this._setVisible(true); +        if (this._child !== null) { +            this._child.hide(true); +        } +    } + +    _setVisible(visible) { +        this._visible = visible; +        this._updateVisibility(); +    } + +    _updateVisibility() { +        this._container.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; +            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 is needed for Chrome. +            window.focus(); +        } +    } + +    _getSiteColor() { +        const color = [255, 255, 255]; +        Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); +        Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.body).backgroundColor)); +        const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); +        return dark ? 'dark' : 'light'; +    } + +    _invokeApi(action, params={}) { +        this._container.contentWindow.postMessage({action, params}, '*'); +    } + +    _observeFullscreen() { +        const fullscreenEvents = [ +            'fullscreenchange', +            'MSFullscreenChange', +            'mozfullscreenchange', +            'webkitfullscreenchange' +        ]; +        for (const eventName of fullscreenEvents) { +            document.addEventListener(eventName, () => this._onFullscreenChanged(), false);          }      } -    static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { +    _getFullscreenElement() { +        return ( +            document.fullscreenElement || +            document.msFullscreenElement || +            document.mozFullScreenElement || +            document.webkitFullscreenElement +        ); +    } + +    _onFullscreenChanged() { +        const parent = (this._getFullscreenElement() || document.body || null); +        if (parent !== null && this._container.parentNode !== parent) { +            parent.appendChild(this._container); +        } +    } + +    static _getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) {          let x = elementRect.left + optionsGeneral.popupHorizontalOffset;          const overflowX = Math.max(x + width - maxWidth, 0);          if (overflowX > 0) { @@ -147,7 +353,7 @@ class Popup {          const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below');          const verticalOffset = optionsGeneral.popupVerticalOffset; -        const [y, h, below] = Popup.limitGeometry( +        const [y, h, below] = Popup._limitGeometry(              elementRect.top - verticalOffset,              elementRect.bottom + verticalOffset,              height, @@ -158,19 +364,19 @@ class Popup {          return [x, y, width, h, below];      } -    static getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { -        const preferRight = Popup.isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); +    static _getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { +        const preferRight = Popup._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode);          const horizontalOffset = optionsGeneral.popupHorizontalOffset2;          const verticalOffset = optionsGeneral.popupVerticalOffset2; -        const [x, w] = Popup.limitGeometry( +        const [x, w] = Popup._limitGeometry(              elementRect.left - horizontalOffset,              elementRect.right + horizontalOffset,              width,              maxWidth,              preferRight          ); -        const [y, h, below] = Popup.limitGeometry( +        const [y, h, below] = Popup._limitGeometry(              elementRect.bottom - verticalOffset,              elementRect.top + verticalOffset,              height, @@ -180,12 +386,12 @@ class Popup {          return [x, y, w, h, below];      } -    static isVerticalTextPopupOnRight(positionPreference, writingMode) { +    static _isVerticalTextPopupOnRight(positionPreference, writingMode) {          switch (positionPreference) {              case 'before': -                return !Popup.isWritingModeLeftToRight(writingMode); +                return !Popup._isWritingModeLeftToRight(writingMode);              case 'after': -                return Popup.isWritingModeLeftToRight(writingMode); +                return Popup._isWritingModeLeftToRight(writingMode);              case 'left':                  return false;              case 'right': @@ -193,7 +399,7 @@ class Popup {          }      } -    static isWritingModeLeftToRight(writingMode) { +    static _isWritingModeLeftToRight(writingMode) {          switch (writingMode) {              case 'vertical-lr':              case 'sideways-lr': @@ -203,7 +409,7 @@ class Popup {          }      } -    static limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) { +    static _limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) {          let after = preferAfter;          let position = 0;          const overflowBefore = Math.max(0, size - positionBefore); @@ -225,72 +431,7 @@ class Popup {          return [position, size, after];      } -    hide(changeFocus) { -        if (!this.isVisible()) { -            return; -        } - -        this.setVisible(false); -        if (this.child !== null) { -            this.child.hide(false); -        } -        if (changeFocus) { -            this.focusParent(); -        } -    } - -    async isVisibleAsync() { -        return this.isVisible(); -    } - -    isVisible() { -        return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible); -    } - -    setVisible(visible) { -        this.visible = visible; -        this.updateVisibility(); -    } - -    setVisibleOverride(visible) { -        this.visibleOverride = visible; -        this.updateVisibility(); -    } - -    updateVisibility() { -        this.container.style.setProperty('visibility', this.isVisible() ? 'visible' : 'hidden', 'important'); -    } - -    focusParent() { -        if (this.parent !== null) { -            // Chrome doesn't like focusing iframe without contentWindow. -            const contentWindow = this.parent.container.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 is needed for Chrome. -            window.focus(); -        } -    } - -    updateTheme() { -        this.container.dataset.yomichanTheme = this.options.general.popupOuterTheme; -        this.container.dataset.yomichanSiteColor = this.getSiteColor(); -    } - -    getSiteColor() { -        const color = [255, 255, 255]; -        Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); -        Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.body).backgroundColor)); -        const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); -        return dark ? 'dark' : 'light'; -    } - -    static addColor(target, color) { +    static _addColor(target, color) {          if (color === null) { return; }          const a = color[3]; @@ -302,7 +443,7 @@ class Popup {          }      } -    static getColorInfo(cssColor) { +    static _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; } @@ -315,80 +456,7 @@ class Popup {          ];      } -    async containsPoint(x, y) { -        for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) { -            const rect = popup.container.getBoundingClientRect(); -            if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { -                return true; -            } -        } -        return false; -    } - -    async setCustomCss(css) { -        this.invokeApi('setCustomCss', {css}); -    } - -    async setCustomOuterCss(css, injectDirectly) { -        // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. -        if (this.stylesheetInjectedViaApi) { return; } - -        if (injectDirectly || Popup.isOnExtensionPage()) { -            Popup.injectOuterStylesheet(css); -        } else { -            if (!css) { return; } -            try { -                await apiInjectStylesheet(css); -                this.stylesheetInjectedViaApi = true; -            } catch (e) { -                // NOP -            } -        } -    } - -    clearAutoPlayTimer() { -        if (this.isInjected) { -            this.invokeApi('clearAutoPlayTimer'); -        } -    } - -    invokeApi(action, params={}) { -        this.container.contentWindow.postMessage({action, params}, '*'); -    } - -    observeFullscreen() { -        const fullscreenEvents = [ -            'fullscreenchange', -            'MSFullscreenChange', -            'mozfullscreenchange', -            'webkitfullscreenchange' -        ]; -        for (const eventName of fullscreenEvents) { -            document.addEventListener(eventName, () => this.onFullscreenChanged(), false); -        } -    } - -    getFullscreenElement() { -        return ( -            document.fullscreenElement || -            document.msFullscreenElement || -            document.mozFullScreenElement || -            document.webkitFullscreenElement -        ); -    } - -    onFullscreenChanged() { -        const parent = (this.getFullscreenElement() || document.body || null); -        if (parent !== null && this.container.parentNode !== parent) { -            parent.appendChild(this.container); -        } -    } - -    get url() { -        return window.location.href; -    } - -    static isOnExtensionPage() { +    static _isOnExtensionPage() {          try {              const url = chrome.runtime.getURL('/');              return window.location.href.substring(0, url.length) === url; @@ -396,26 +464,6 @@ class Popup {              // NOP          }      } - -    static injectOuterStylesheet(css) { -        if (Popup.outerStylesheet === null) { -            if (!css) { return; } -            Popup.outerStylesheet = document.createElement('style'); -            Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet'; -        } - -        const outerStylesheet = Popup.outerStylesheet; -        if (css) { -            outerStylesheet.textContent = css; - -            const par = document.head; -            if (par && outerStylesheet.parentNode !== par) { -                par.appendChild(outerStylesheet); -            } -        } else { -            outerStylesheet.textContent = ''; -        } -    }  }  Popup.outerStylesheet = null; |