diff options
| author | Alex Yatskov <alex@foosoft.net> | 2020-01-26 11:29:30 -0800 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2020-01-26 11:29:30 -0800 | 
| commit | 0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (patch) | |
| tree | e734e2c3005078dbc248b541d357a934baa8a116 /ext/fg/js | |
| parent | 2a12036ca305044291f1f4105d6a8d249848b210 (diff) | |
| parent | 0cf1cf3aa094585bd6db8db2c1f229ba0ea37b6e (diff) | |
Merge branch 'master' into testing
Diffstat (limited to 'ext/fg/js')
| -rw-r--r-- | ext/fg/js/document.js | 15 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 13 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 94 | ||||
| -rw-r--r-- | ext/fg/js/popup-nested.js | 2 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy-host.js | 8 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy.js | 5 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 173 | ||||
| -rw-r--r-- | ext/fg/js/source.js | 19 | 
8 files changed, 246 insertions, 83 deletions
| diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index e068e3ba..71654b29 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -110,6 +110,7 @@ function docRangeFromPoint(x, y, deepDomScan) {      const elements = docElementsFromPoint(x, y, deepDomScan);      let imposter = null;      let imposterContainer = null; +    let imposterSourceElement = null;      if (elements.length > 0) {          const element = elements[0];          switch (element.nodeName.toUpperCase()) { @@ -117,9 +118,11 @@ function docRangeFromPoint(x, y, deepDomScan) {              case 'BUTTON':                  return new TextSourceElement(element);              case 'INPUT': +                imposterSourceElement = element;                  [imposter, imposterContainer] = docImposterCreate(element, false);                  break;              case 'TEXTAREA': +                imposterSourceElement = element;                  [imposter, imposterContainer] = docImposterCreate(element, true);                  break;          } @@ -131,7 +134,7 @@ function docRangeFromPoint(x, y, deepDomScan) {              docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646');              docSetImposterStyle(imposter.style, 'pointer-events', 'none');          } -        return new TextSourceRange(range, '', imposterContainer); +        return new TextSourceRange(range, '', imposterContainer, imposterSourceElement);      } else {          if (imposterContainer !== null) {              imposterContainer.parentNode.removeChild(imposterContainer); @@ -269,8 +272,14 @@ const caretRangeFromPoint = (() => {              const range = document.createRange();              const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); -            range.setStart(node, offset); -            range.setEnd(node, offset); +            try { +                range.setStart(node, offset); +                range.setEnd(node, offset); +            } catch (e) { +                // Firefox throws new DOMException("The operation is insecure.") +                // when trying to select a node from within a ShadowRoot. +                return null; +            }              return range;          };      } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 513d246b..8d61d8f6 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -35,7 +35,7 @@ class DisplayFloat extends Display {      onError(error) {          if (this._orphaned) { -            this.setContentOrphaned(); +            this.setContent('orphaned');          } else {              logError(error, true);          } @@ -89,7 +89,11 @@ class DisplayFloat extends Display {          }      } -    async initialize(options, popupInfo, url, childrenSupported) { +    setContentScale(scale) { +        document.body.style.fontSize = `${scale}em`; +    } + +    async initialize(options, popupInfo, url, childrenSupported, scale) {          await super.initialize(options);          const {id, depth, parentFrameId} = popupInfo; @@ -99,6 +103,8 @@ class DisplayFloat extends Display {          if (childrenSupported) {              popupNestedInitialize(id, depth, parentFrameId, url);          } + +        this.setContentScale(scale);      }  } @@ -116,7 +122,8 @@ DisplayFloat._messageHandlers = new Map([      ['setContent', (self, {type, details}) => self.setContent(type, details)],      ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()],      ['setCustomCss', (self, {css}) => self.setCustomCss(css)], -    ['initialize', (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported)] +    ['initialize', (self, {options, popupInfo, url, childrenSupported, scale}) => self.initialize(options, popupInfo, url, childrenSupported, scale)], +    ['setContentScale', (self, {scale}) => self.setContentScale(scale)]  ]);  DisplayFloat.instance = new DisplayFloat(); diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 034d9075..2286bf19 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -34,6 +34,8 @@ class Frontend extends TextScanner {              url: popup.url          }; +        this._pageZoomFactor = 1.0; +        this._contentScale = 1.0;          this._orphaned = true;          this._lastShowPromise = Promise.resolve();      } @@ -41,23 +43,30 @@ class Frontend extends TextScanner {      async prepare() {          try {              await this.updateOptions(); +            const {zoomFactor} = await apiGetZoom(); +            this._pageZoomFactor = zoomFactor; + +            window.addEventListener('resize', this.onResize.bind(this), false); + +            const visualViewport = window.visualViewport; +            if (visualViewport !== null && typeof visualViewport === 'object') { +                window.visualViewport.addEventListener('scroll', this.onVisualViewportScroll.bind(this)); +                window.visualViewport.addEventListener('resize', this.onVisualViewportResize.bind(this)); +            }              yomichan.on('orphaned', () => this.onOrphaned());              yomichan.on('optionsUpdate', () => this.updateOptions()); +            yomichan.on('zoomChanged', (e) => this.onZoomChanged(e));              chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); + +            this._updateContentScale();          } catch (e) {              this.onError(e);          }      } -    async onResize() { -        const textSource = this.textSourceCurrent; -        if (textSource !== null && await this.popup.isVisible()) { -            this._lastShowPromise = this.popup.showContent( -                textSource.getRect(), -                textSource.getWritingMode() -            ); -        } +    onResize() { +        this._updatePopupPosition();      }      onWindowMessage(e) { @@ -81,18 +90,30 @@ class Frontend extends TextScanner {          this._orphaned = true;      } +    onZoomChanged({newZoomFactor}) { +        this._pageZoomFactor = newZoomFactor; +        this._updateContentScale(); +    } + +    onVisualViewportScroll() { +        this._updatePopupPosition(); +    } + +    onVisualViewportResize() { +        this._updateContentScale(); +    } +      getMouseEventListeners() {          return [              ...super.getMouseEventListeners(), -            [window, 'message', this.onWindowMessage.bind(this)], -            [window, 'resize', this.onResize.bind(this)] +            [window, 'message', this.onWindowMessage.bind(this)]          ];      }      async updateOptions() { -        this.options = await apiOptionsGet(this.getOptionsContext()); +        this.setOptions(await apiOptionsGet(this.getOptionsContext()));          await this.popup.setOptions(this.options); -        this.setEnabled(this.options.general.enable); +        this._updateContentScale();      }      async onSearchSource(textSource, cause) { @@ -112,11 +133,7 @@ class Frontend extends TextScanner {          } catch (e) {              if (this._orphaned) {                  if (textSource !== null && this.options.scanning.modifier !== 'none') { -                    this._lastShowPromise = this.popup.showContent( -                        textSource.getRect(), -                        textSource.getWritingMode(), -                        'orphaned' -                    ); +                    this._showPopupContent(textSource, 'orphaned');                  }              } else {                  this.onError(e); @@ -133,9 +150,8 @@ class Frontend extends TextScanner {      showContent(textSource, focus, definitions, type) {          const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);          const url = window.location.href; -        this._lastShowPromise = this.popup.showContent( -            textSource.getRect(), -            textSource.getWritingMode(), +        this._showPopupContent( +            textSource,              type,              {definitions, context: {sentence, url, focus, disableHistory: true}}          ); @@ -181,6 +197,44 @@ class Frontend extends TextScanner {          this.optionsContext.url = this.popup.url;          return this.optionsContext;      } + +    _showPopupContent(textSource, type=null, details=null) { +        this._lastShowPromise = this.popup.showContent( +            textSource.getRect(), +            textSource.getWritingMode(), +            type, +            details +        ); +        return this._lastShowPromise; +    } + +    _updateContentScale() { +        const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this.options.general; +        let contentScale = popupScalingFactor; +        if (popupScaleRelativeToPageZoom) { +            contentScale /= this._pageZoomFactor; +        } +        if (popupScaleRelativeToVisualViewport) { +            contentScale /= Frontend._getVisualViewportScale(); +        } +        if (contentScale === this._contentScale) { return; } + +        this._contentScale = contentScale; +        this.popup.setContentScale(this._contentScale); +        this._updatePopupPosition(); +    } + +    async _updatePopupPosition() { +        const textSource = this.getCurrentTextSource(); +        if (textSource !== null && await this.popup.isVisible()) { +            this._showPopupContent(textSource); +        } +    } + +    static _getVisualViewportScale() { +        const visualViewport = window.visualViewport; +        return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0; +    }  }  Frontend._windowMessageHandlers = new Map([ diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index bacf3b93..3f3c945e 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -35,7 +35,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {      const ignoreNodes = ['.scan-disable', '.scan-disable *'];      if (!options.scanning.enableOnPopupExpressions) { -        ignoreNodes.push('.expression-scan-toggle', '.expression-scan-toggle *'); +        ignoreNodes.push('.source-text', '.source-text *');      }      window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true}; diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index c4f0c6ff..427172c6 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -41,7 +41,8 @@ class PopupProxyHost {              ['containsPoint', ({id, x, y}) => this._onApiContainsPoint(id, x, y)],              ['showContent', ({id, elementRect, writingMode, type, details}) => this._onApiShowContent(id, elementRect, writingMode, type, details)],              ['setCustomCss', ({id, css}) => this._onApiSetCustomCss(id, css)], -            ['clearAutoPlayTimer', ({id}) => this._onApiClearAutoPlayTimer(id)] +            ['clearAutoPlayTimer', ({id}) => this._onApiClearAutoPlayTimer(id)], +            ['setContentScale', ({id, scale}) => this._onApiSetContentScale(id, scale)]          ]));      } @@ -97,6 +98,11 @@ class PopupProxyHost {          return popup.clearAutoPlayTimer();      } +    async _onApiSetContentScale(id, scale) { +        const popup = this._getPopup(id); +        return popup.setContentScale(scale); +    } +      // Private functions      _createPopupInternal(parentId, depth) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index ae0cffad..4cacee53 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -97,6 +97,11 @@ class PopupProxy {          this._invokeHostApi('clearAutoPlayTimer', {id: this._id});      } +    async setContentScale(scale) { +        const id = await this._getPopupId(); +        this._invokeHostApi('setContentScale', {id, scale}); +    } +      // Private      _getPopupId() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 7a0c6133..e7dae93e 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -28,10 +28,13 @@ class Popup {          this._childrenSupported = true;          this._injectPromise = null;          this._isInjected = false; +        this._isInjectedAndLoaded = false;          this._visible = false;          this._visibleOverride = null;          this._options = null;          this._stylesheetInjectedViaApi = false; +        this._contentScale = 1.0; +        this._containerSizeContentScale = null;          this._container = document.createElement('iframe');          this._container.className = 'yomichan-float'; @@ -103,7 +106,7 @@ class Popup {      }      async showContent(elementRect, writingMode, type=null, details=null) { -        if (!this._isInitialized()) { return; } +        if (this._options === null) { throw new Error('Options not assigned'); }          await this._show(elementRect, writingMode);          if (type === null) { return; }          this._invokeApi('setContent', {type, details}); @@ -114,11 +117,18 @@ class Popup {      }      clearAutoPlayTimer() { -        if (this._isInjected) { +        if (this._isInjectedAndLoaded) {              this._invokeApi('clearAutoPlayTimer');          }      } +    setContentScale(scale) { +        this._contentScale = scale; +        if (this._isInjectedAndLoaded) { +            this._invokeApi('setContentScale', {scale}); +        } +    } +      // Popup-only public functions      setParent(parent) { @@ -215,6 +225,7 @@ class Popup {          return new Promise((resolve) => {              const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);              this._container.addEventListener('load', () => { +                this._isInjectedAndLoaded = true;                  this._invokeApi('initialize', {                      options: this._options,                      popupInfo: { @@ -223,7 +234,8 @@ class Popup {                          parentFrameId                      },                      url: this.url, -                    childrenSupported: this._childrenSupported +                    childrenSupported: this._childrenSupported, +                    scale: this._contentScale                  });                  resolve();              }); @@ -234,10 +246,6 @@ class Popup {          });      } -    _isInitialized() { -        return this._options !== null; -    } -      async _show(elementRect, writingMode) {          await this._inject(); @@ -250,18 +258,30 @@ class Popup {              Popup._getPositionForVerticalText          ); -        const [x, y, width, height, below] = getPosition( +        const viewport = Popup._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(              elementRect, -            Math.max(containerRect.width, optionsGeneral.popupWidth), -            Math.max(containerRect.height, optionsGeneral.popupHeight), -            document.body.clientWidth, -            window.innerHeight, +            Math.max(containerRect.width * scaleRatio, optionsGeneral.popupWidth * scale), +            Math.max(containerRect.height * scaleRatio, optionsGeneral.popupHeight * scale), +            viewport, +            scale,              optionsGeneral,              writingMode          ); -        container.classList.toggle('yomichan-float-full-width', optionsGeneral.popupDisplayMode === 'full-width'); +        const fullWidth = (optionsGeneral.popupDisplayMode === 'full-width'); +        container.classList.toggle('yomichan-float-full-width', fullWidth);          container.classList.toggle('yomichan-float-above', !below); + +        if (optionsGeneral.popupDisplayMode === 'full-width') { +            x = viewport.left; +            y = below ? viewport.bottom - height : viewport.top; +            width = viewport.right - viewport.left; +        } +          container.style.left = `${x}px`;          container.style.top = `${y}px`;          container.style.width = `${width}px`; @@ -307,6 +327,9 @@ class Popup {      }      _invokeApi(action, params={}) { +        if (!this._isInjectedAndLoaded) { +            throw new Error('Frame not loaded'); +        }          this._container.contentWindow.postMessage({action, params}, '*');      } @@ -338,49 +361,49 @@ class Popup {          }      } -    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) { -            if (x >= overflowX) { -                x -= overflowX; -            } else { -                width = maxWidth; -                x = 0; -            } -        } - +    static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {          const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); +        const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; +        const verticalOffset = optionsGeneral.popupVerticalOffset * offsetScale; -        const verticalOffset = optionsGeneral.popupVerticalOffset; -        const [y, h, below] = Popup._limitGeometry( +        const [x, w] = Popup._getConstrainedPosition( +            elementRect.right - horizontalOffset, +            elementRect.left + horizontalOffset, +            width, +            viewport.left, +            viewport.right, +            true +        ); +        const [y, h, below] = Popup._getConstrainedPositionBinary(              elementRect.top - verticalOffset,              elementRect.bottom + verticalOffset,              height, -            maxHeight, +            viewport.top, +            viewport.bottom,              preferBelow          ); - -        return [x, y, width, h, below]; +        return [x, y, w, h, below];      } -    static _getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { +    static _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) {          const preferRight = Popup._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); -        const horizontalOffset = optionsGeneral.popupHorizontalOffset2; -        const verticalOffset = optionsGeneral.popupVerticalOffset2; +        const horizontalOffset = optionsGeneral.popupHorizontalOffset2 * offsetScale; +        const verticalOffset = optionsGeneral.popupVerticalOffset2 * offsetScale; -        const [x, w] = Popup._limitGeometry( +        const [x, w] = Popup._getConstrainedPositionBinary(              elementRect.left - horizontalOffset,              elementRect.right + horizontalOffset,              width, -            maxWidth, +            viewport.left, +            viewport.right,              preferRight          ); -        const [y, h, below] = Popup._limitGeometry( +        const [y, h, below] = Popup._getConstrainedPosition(              elementRect.bottom - verticalOffset,              elementRect.top + verticalOffset,              height, -            maxHeight, +            viewport.top, +            viewport.bottom,              true          );          return [x, y, w, h, below]; @@ -409,23 +432,36 @@ class Popup {          }      } -    static _limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) { -        let after = preferAfter; -        let position = 0; -        const overflowBefore = Math.max(0, size - positionBefore); -        const overflowAfter = Math.max(0, positionAfter + size - limit); +    static _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]; +    } + +    static _getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) { +        const overflowBefore = minLimit - (positionBefore - size); +        const overflowAfter = (positionAfter + size) - maxLimit; +          if (overflowAfter > 0 || overflowBefore > 0) { -            if (overflowAfter < overflowBefore) { -                size = Math.max(0, size - overflowAfter); -                position = positionAfter; -                after = true; -            } else { -                size = Math.max(0, size - overflowBefore); -                position = Math.max(0, positionBefore - size); -                after = false; -            } +            after = (overflowAfter < overflowBefore); +        } + +        let position; +        if (after) { +            size -= Math.max(0, overflowAfter); +            position = Math.max(minLimit, positionAfter);          } else { -            position = preferAfter ? positionAfter : positionBefore - size; +            size -= Math.max(0, overflowBefore); +            position = Math.min(maxLimit, positionBefore) - size;          }          return [position, size, after]; @@ -464,6 +500,39 @@ class Popup {              // NOP          }      } + +    static _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) +                }; +            } +        } + +        return { +            left: 0, +            top: 0, +            right: document.body.clientWidth, +            bottom: window.innerHeight +        }; +    }  }  Popup.outerStylesheet = null; diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 5cdf47b5..11d3ff0e 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -25,14 +25,16 @@ const IGNORE_TEXT_PATTERN = /\u200c/;   */  class TextSourceRange { -    constructor(range, content, imposterContainer) { +    constructor(range, content, imposterContainer, imposterSourceElement) {          this.range = range; +        this.rangeStartOffset = range.startOffset;          this.content = content;          this.imposterContainer = imposterContainer; +        this.imposterSourceElement = imposterSourceElement;      }      clone() { -        return new TextSourceRange(this.range.cloneRange(), this.content, this.imposterContainer); +        return new TextSourceRange(this.range.cloneRange(), this.content, this.imposterContainer, this.imposterSourceElement);      }      cleanup() { @@ -55,6 +57,7 @@ class TextSourceRange {      setStartOffset(length) {          const state = TextSourceRange.seekBackward(this.range.startContainer, this.range.startOffset, length);          this.range.setStart(state.node, state.offset); +        this.rangeStartOffset = this.range.startOffset;          this.content = state.content;          return length - state.remainder;      } @@ -79,7 +82,17 @@ class TextSourceRange {      }      equals(other) { -        return other && other.range && other.range.compareBoundaryPoints(Range.START_TO_START, this.range) === 0; +        if (other === null) { +            return false; +        } +        if (this.imposterSourceElement !== null) { +            return ( +                this.imposterSourceElement === other.imposterSourceElement && +                this.rangeStartOffset === other.rangeStartOffset +            ); +        } else { +            return this.range.compareBoundaryPoints(Range.START_TO_START, other.range) === 0; +        }      }      static shouldEnter(node) { |