Skip to content
This repository has been archived by the owner on Feb 25, 2023. It is now read-only.

Commit

Permalink
Exclude documentElement from zoom calculation (#2227)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
toasted-nutbread committed Sep 21, 2022
1 parent ac373a6 commit 480869c
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 38 deletions.
7 changes: 6 additions & 1 deletion ext/data/schemas/options-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@
"layoutAwareScan",
"matchTypePrefix",
"hidePopupOnCursorExit",
"hidePopupOnCursorExitDelay"
"hidePopupOnCursorExitDelay",
"normalizeCssZoom"
],
"properties": {
"inputs": {
Expand Down Expand Up @@ -706,6 +707,10 @@
"type": "number",
"minimum": 0,
"default": 0
},
"normalizeCssZoom": {
"type": "boolean",
"default": true
}
}
},
Expand Down
1 change: 1 addition & 0 deletions ext/js/app/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
58 changes: 49 additions & 9 deletions ext/js/app/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}

Expand All @@ -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};
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
};
}
}
2 changes: 2 additions & 0 deletions ext/js/data/options-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions ext/js/display/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1532,6 +1533,7 @@ class Display extends EventDispatcher {
}
}],
deepContentScan: scanningOptions.deepDomScan,
normalizeCssZoom: scanningOptions.normalizeCssZoom,
selectText: false,
delay: scanningOptions.delay,
touchInputEnabled: false,
Expand Down
87 changes: 65 additions & 22 deletions ext/js/dom/document-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -435,16 +488,18 @@ 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) {
return false;
}

// 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
Expand Down Expand Up @@ -583,7 +638,7 @@ class DocumentUtil {
}
}

_caretRangeFromPointExt(x, y, elements) {
_caretRangeFromPointExt(x, y, elements, normalizeCssZoom) {
let previousStyles = null;
try {
let i = 0;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
5 changes: 3 additions & 2 deletions ext/js/dom/text-source-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

/* global
* DocumentUtil
* StringUtil
*/

Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions ext/js/dom/text-source-range.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
10 changes: 9 additions & 1 deletion ext/js/language/text-scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +152,7 @@ class TextScanner extends EventDispatcher {
setOptions({
inputs,
deepContentScan,
normalizeCssZoom,
selectText,
delay,
touchInputEnabled,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 480869c

Please sign in to comment.