aboutsummaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2022-09-20 21:06:39 -0400
committerGitHub <noreply@github.com>2022-09-20 21:06:39 -0400
commit480869c3d1d820b344d23989d2deae64a594869e (patch)
treebc1d0c5143b71e312322e3dc851cb2c98050b8c6 /ext/js
parentac373a67944a3241f96091b2bcd94f1b337a3940 (diff)
Exclude documentElement from zoom calculation (#2227)
* Exclude documentElement from zoom calculation * Add an option * Refactor zoom coordinate conversion functions * Convert zoom coordinates for text sources * Rename variable * Convert rect coordinate spaces * Handle shadow DOM
Diffstat (limited to 'ext/js')
-rw-r--r--ext/js/app/frontend.js1
-rw-r--r--ext/js/app/popup.js58
-rw-r--r--ext/js/data/options-util.js2
-rw-r--r--ext/js/display/display.js2
-rw-r--r--ext/js/dom/document-util.js87
-rw-r--r--ext/js/dom/text-source-element.js5
-rw-r--r--ext/js/dom/text-source-range.js4
-rw-r--r--ext/js/language/text-scanner.js10
8 files changed, 133 insertions, 36 deletions
diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js
index f3b925c3..de3eb7fd 100644
--- a/ext/js/app/frontend.js
+++ b/ext/js/app/frontend.js
@@ -395,6 +395,7 @@ class Frontend {
this._textScanner.setOptions({
inputs: scanningOptions.inputs,
deepContentScan: scanningOptions.deepDomScan,
+ normalizeCssZoom: scanningOptions.normalizeCssZoom,
selectText: scanningOptions.selectText,
delay: scanningOptions.delay,
touchInputEnabled: scanningOptions.touchInputEnabled,
diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js
index 9ca2165d..8b62b92a 100644
--- a/ext/js/app/popup.js
+++ b/ext/js/app/popup.js
@@ -368,7 +368,7 @@ class Popup extends EventDispatcher {
* `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
*/
getFrameRect() {
- const {left, top, right, bottom} = this._frame.getBoundingClientRect();
+ const {left, top, right, bottom} = this._getFrameBoundingClientRect();
return {left, top, right, bottom, valid: true};
}
@@ -377,7 +377,7 @@ class Popup extends EventDispatcher {
* @returns {Promise<{width: number, height: number, valid: boolean}>} The size and whether or not it is valid.
*/
async getFrameSize() {
- const {width, height} = this._frame.getBoundingClientRect();
+ const {width, height} = this._getFrameBoundingClientRect();
return {width, height, valid: true};
}
@@ -680,12 +680,13 @@ class Popup extends EventDispatcher {
* @returns {SizeRect} The calculated rectangle for where to position the popup.
*/
_getPosition(sourceRects, writingMode, viewport) {
- const scale = this._contentScale;
- const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale;
- this._frameSizeContentScale = scale;
+ sourceRects = this._convertSourceRectsCoordinateSpace(sourceRects);
+ const contentScale = this._contentScale;
+ const scaleRatio = this._frameSizeContentScale === null ? 1.0 : contentScale / this._frameSizeContentScale;
+ this._frameSizeContentScale = contentScale;
const frameRect = this._frame.getBoundingClientRect();
- const frameWidth = Math.max(frameRect.width * scaleRatio, this._initialWidth * scale);
- const frameHeight = Math.max(frameRect.height * scaleRatio, this._initialHeight * scale);
+ const frameWidth = Math.max(frameRect.width * scaleRatio, this._initialWidth * contentScale);
+ const frameHeight = Math.max(frameRect.height * scaleRatio, this._initialHeight * contentScale);
const horizontal = (writingMode === 'horizontal-tb' || this._verticalTextPosition === 'default');
let preferAfter;
@@ -700,8 +701,8 @@ class Popup extends EventDispatcher {
horizontalOffset = this._horizontalOffset2;
verticalOffset = this._verticalOffset2;
}
- horizontalOffset *= scale;
- verticalOffset *= scale;
+ horizontalOffset *= contentScale;
+ verticalOffset *= contentScale;
let best = null;
const sourceRectsLength = sourceRects.length;
@@ -955,4 +956,43 @@ class Popup extends EventDispatcher {
}
return false;
}
+
+ /**
+ * Gets the bounding client rect for the frame element, with a coordinate conversion applied.
+ * @returns {DOMRect} The rectangle of the frame.
+ */
+ _getFrameBoundingClientRect() {
+ return DocumentUtil.convertRectZoomCoordinates(this._frame.getBoundingClientRect(), this._container);
+ }
+
+ /**
+ * Converts the coordinate space of source rectangles.
+ * @param {Rect[]} sourceRects The list of rectangles to convert.
+ * @returns {Rect[]} Either an updated list of rectangles, or `sourceRects` if no change is required.
+ */
+ _convertSourceRectsCoordinateSpace(sourceRects) {
+ let scale = DocumentUtil.computeZoomScale(this._container);
+ if (scale === 1) { return sourceRects; }
+ scale = 1 / scale;
+ const sourceRects2 = [];
+ for (const rect of sourceRects) {
+ sourceRects2.push(this._createScaledRect(rect, scale));
+ }
+ return sourceRects2;
+ }
+
+ /**
+ * Creates a scaled rectangle.
+ * @param {Rect} rect The rectangle to scale.
+ * @param {number} scale The scale factor.
+ * @returns {Rect} A new rectangle which has been scaled.
+ */
+ _createScaledRect(rect, scale) {
+ return {
+ left: rect.left * scale,
+ top: rect.top * scale,
+ right: rect.right * scale,
+ bottom: rect.bottom * scale
+ };
+ }
}
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index d2835adb..442007d0 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -980,10 +980,12 @@ class OptionsUtil {
_updateVersion20(options) {
// Version 20 changes:
// Added anki.downloadTimeout.
+ // Added scanning.normalizeCssZoom.
// Fixed general.popupTheme invalid default.
// Fixed general.popupOuterTheme invalid default.
for (const profile of options.profiles) {
profile.options.anki.downloadTimeout = 0;
+ profile.options.scanning.normalizeCssZoom = true;
const {general} = profile.options;
if (general.popupTheme === 'default') {
general.popupTheme = 'light';
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index 8905d304..d286bd5e 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -367,6 +367,7 @@ class Display extends EventDispatcher {
scanning: {
inputs: scanningOptions.inputs,
deepContentScan: scanningOptions.deepDomScan,
+ normalizeCssZoom: scanningOptions.normalizeCssZoom,
selectText: scanningOptions.selectText,
delay: scanningOptions.delay,
touchInputEnabled: scanningOptions.touchInputEnabled,
@@ -1532,6 +1533,7 @@ class Display extends EventDispatcher {
}
}],
deepContentScan: scanningOptions.deepDomScan,
+ normalizeCssZoom: scanningOptions.normalizeCssZoom,
selectText: false,
delay: scanningOptions.delay,
touchInputEnabled: false,
diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js
index b974387e..41f44afe 100644
--- a/ext/js/dom/document-util.js
+++ b/ext/js/dom/document-util.js
@@ -24,10 +24,9 @@
class DocumentUtil {
constructor() {
this._transparentColorPattern = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/;
- this._cssZoomSupported = (typeof document.createElement('div').style.zoom === 'string');
}
- getRangeFromPoint(x, y, deepContentScan) {
+ getRangeFromPoint(x, y, {deepContentScan, normalizeCssZoom}) {
const elements = this._getElementsFromPoint(x, y, deepContentScan);
let imposter = null;
let imposterContainer = null;
@@ -52,7 +51,7 @@ class DocumentUtil {
}
}
- const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : []);
+ const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : [], normalizeCssZoom);
if (range !== null) {
if (imposter !== null) {
this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
@@ -175,6 +174,60 @@ class DocumentUtil {
};
}
+ /**
+ * Computes the scaling adjustment that is necessary for client space coordinates based on the
+ * CSS zoom level.
+ * @param {Node} node A node in the document.
+ * @returns {number} The scaling factor.
+ */
+ static computeZoomScale(node) {
+ if (this._cssZoomSupported === null) {
+ this._cssZoomSupported = (typeof document.createElement('div').style.zoom === 'string');
+ }
+ if (!this._cssZoomSupported) { return 1; }
+ // documentElement must be excluded because the computer style of its zoom property is inconsistent.
+ // * If CSS `:root{zoom:X;}` is specified, the computed zoom will always report `X`.
+ // * If CSS `:root{zoom:X;}` is not specified, the computed zoom report the browser's zoom level.
+ // Therefor, if CSS root zoom is specified as a value other than 1, the adjusted {x, y} values
+ // would be incorrect, which is not new behaviour.
+ let scale = 1;
+ const {ELEMENT_NODE, DOCUMENT_FRAGMENT_NODE} = Node;
+ const {documentElement} = document;
+ for (; node !== null && node !== documentElement; node = node.parentNode) {
+ const {nodeType} = node;
+ if (nodeType === DOCUMENT_FRAGMENT_NODE) {
+ const {host} = node;
+ if (typeof host !== 'undefined') {
+ node = host;
+ }
+ continue;
+ } else if (nodeType !== ELEMENT_NODE) {
+ continue;
+ }
+ let {zoom} = getComputedStyle(node);
+ if (typeof zoom !== 'string') { continue; }
+ zoom = Number.parseFloat(zoom);
+ if (!Number.isFinite(zoom) || zoom === 0) { continue; }
+ scale *= zoom;
+ }
+ return scale;
+ }
+
+ static convertRectZoomCoordinates(rect, node) {
+ const scale = this.computeZoomScale(node);
+ return (scale === 1 ? rect : new DOMRect(rect.left * scale, rect.top * scale, rect.width * scale, rect.height * scale));
+ }
+
+ static convertMultipleRectZoomCoordinates(rects, node) {
+ const scale = this.computeZoomScale(node);
+ if (scale === 1) { return rects; }
+ const results = [];
+ for (const rect of rects) {
+ results.push(new DOMRect(rect.left * scale, rect.top * scale, rect.width * scale, rect.height * scale));
+ }
+ return results;
+ }
+
static isPointInRect(x, y, rect) {
return (
x >= rect.left && x < rect.right &&
@@ -435,7 +488,7 @@ class DocumentUtil {
return e !== null ? [e] : [];
}
- _isPointInRange(x, y, range) {
+ _isPointInRange(x, y, range, normalizeCssZoom) {
// Require a text node to start
const {startContainer} = range;
if (startContainer.nodeType !== Node.TEXT_NODE) {
@@ -443,8 +496,10 @@ class DocumentUtil {
}
// Convert CSS zoom coordinates
- if (this._cssZoomSupported) {
- ({x, y} = this._convertCssZoomCoordinates(x, y, startContainer));
+ if (normalizeCssZoom) {
+ const scale = DocumentUtil.computeZoomScale(startContainer);
+ x /= scale;
+ y /= scale;
}
// Scan forward
@@ -583,7 +638,7 @@ class DocumentUtil {
}
}
- _caretRangeFromPointExt(x, y, elements) {
+ _caretRangeFromPointExt(x, y, elements, normalizeCssZoom) {
let previousStyles = null;
try {
let i = 0;
@@ -596,7 +651,7 @@ class DocumentUtil {
const startContainer = range.startContainer;
if (startContinerPre !== startContainer) {
- if (this._isPointInRange(x, y, range)) {
+ if (this._isPointInRange(x, y, range, normalizeCssZoom)) {
return range;
}
startContinerPre = startContainer;
@@ -668,18 +723,6 @@ class DocumentUtil {
_isElementUserSelectAll(element) {
return getComputedStyle(element).userSelect === 'all';
}
-
- _convertCssZoomCoordinates(x, y, node) {
- const ELEMENT_NODE = Node.ELEMENT_NODE;
- for (; node !== null; node = node.parentNode) {
- if (node.nodeType !== ELEMENT_NODE) { continue; }
- let {zoom} = getComputedStyle(node);
- if (typeof zoom !== 'string') { continue; }
- zoom = Number.parseFloat(zoom);
- if (!Number.isFinite(zoom) || zoom === 0) { continue; }
- x /= zoom;
- y /= zoom;
- }
- return {x, y};
- }
}
+// eslint-disable-next-line no-underscore-dangle
+DocumentUtil._cssZoomSupported = null;
diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js
index fe3fe083..b5fc1683 100644
--- a/ext/js/dom/text-source-element.js
+++ b/ext/js/dom/text-source-element.js
@@ -16,6 +16,7 @@
*/
/* global
+ * DocumentUtil
* StringUtil
*/
@@ -95,11 +96,11 @@ class TextSourceElement {
}
getRect() {
- return this._element.getBoundingClientRect();
+ return DocumentUtil.convertRectZoomCoordinates(this._element.getBoundingClientRect(), this._element);
}
getRects() {
- return this._element.getClientRects();
+ return DocumentUtil.convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element);
}
getWritingMode() {
diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js
index 5e3e814c..6c35c4cb 100644
--- a/ext/js/dom/text-source-range.js
+++ b/ext/js/dom/text-source-range.js
@@ -91,11 +91,11 @@ class TextSourceRange {
}
getRect() {
- return this._range.getBoundingClientRect();
+ return DocumentUtil.convertRectZoomCoordinates(this._range.getBoundingClientRect(), this._range.startContainer);
}
getRects() {
- return this._range.getClientRects();
+ return DocumentUtil.convertMultipleRectZoomCoordinates(this._range.getClientRects(), this._range.startContainer);
}
getWritingMode() {
diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js
index 93de4dd4..3b8a8b47 100644
--- a/ext/js/language/text-scanner.js
+++ b/ext/js/language/text-scanner.js
@@ -54,6 +54,7 @@ class TextScanner extends EventDispatcher {
this._selectionRestoreInfo = null;
this._deepContentScan = false;
+ this._normalizeCssZoom = true;
this._selectText = false;
this._delay = 0;
this._touchInputEnabled = false;
@@ -151,6 +152,7 @@ class TextScanner extends EventDispatcher {
setOptions({
inputs,
deepContentScan,
+ normalizeCssZoom,
selectText,
delay,
touchInputEnabled,
@@ -167,6 +169,9 @@ class TextScanner extends EventDispatcher {
if (typeof deepContentScan === 'boolean') {
this._deepContentScan = deepContentScan;
}
+ if (typeof normalizeCssZoom === 'boolean') {
+ this._normalizeCssZoom = normalizeCssZoom;
+ }
if (typeof selectText === 'boolean') {
this._selectText = selectText;
}
@@ -932,7 +937,10 @@ class TextScanner extends EventDispatcher {
return;
}
- const textSource = this._documentUtil.getRangeFromPoint(x, y, this._deepContentScan);
+ const textSource = this._documentUtil.getRangeFromPoint(x, y, {
+ deepContentScan: this._deepContentScan,
+ normalizeCssZoom: this._normalizeCssZoom
+ });
try {
await this._search(textSource, searchTerms, searchKanji, inputInfo);
} finally {