aboutsummaryrefslogtreecommitdiff
path: root/ext/fg/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg/js')
-rw-r--r--ext/fg/js/document.js15
-rw-r--r--ext/fg/js/float.js13
-rw-r--r--ext/fg/js/frontend.js94
-rw-r--r--ext/fg/js/popup-nested.js2
-rw-r--r--ext/fg/js/popup-proxy-host.js8
-rw-r--r--ext/fg/js/popup-proxy.js5
-rw-r--r--ext/fg/js/popup.js173
-rw-r--r--ext/fg/js/source.js19
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) {