diff options
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) { |