aboutsummaryrefslogtreecommitdiff
path: root/ext/js/dom/document-util.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/dom/document-util.js')
-rw-r--r--ext/js/dom/document-util.js290
1 files changed, 212 insertions, 78 deletions
diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js
index d379192c..f53d55fd 100644
--- a/ext/js/dom/document-util.js
+++ b/ext/js/dom/document-util.js
@@ -26,29 +26,11 @@ import {TextSourceRange} from './text-source-range.js';
*/
export class DocumentUtil {
/**
- * Options to configure how element detection is performed.
- * @typedef {object} GetRangeFromPointOptions
- * @property {boolean} deepContentScan Whether or deep content scanning should be performed. When deep content scanning is enabled,
- * some transparent overlay elements will be ignored when looking for the element at the input position.
- * @property {boolean} normalizeCssZoom Whether or not zoom coordinates should be normalized.
- */
-
- /**
- * Scans the document for text or elements with text information at the given coordinate.
- * Coordinates are provided in [client space](https://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_View/Coordinate_systems).
- * @callback GetRangeFromPointHandler
- * @param {number} x The x coordinate to search at.
- * @param {number} y The y coordinate to search at.
- * @param {GetRangeFromPointOptions} options Options to configure how element detection is performed.
- * @returns {?TextSourceRange|TextSourceElement} A range for the hovered text or element, or `null` if no applicable content was found.
- */
-
- /**
* Scans the document for text or elements with text information at the given coordinate.
* Coordinates are provided in [client space](https://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_View/Coordinate_systems).
* @param {number} x The x coordinate to search at.
* @param {number} y The y coordinate to search at.
- * @param {GetRangeFromPointOptions} options Options to configure how element detection is performed.
+ * @param {import('document-util').GetRangeFromPointOptions} options Options to configure how element detection is performed.
* @returns {?TextSourceRange|TextSourceElement} A range for the hovered text or element, or `null` if no applicable content was found.
*/
static getRangeFromPoint(x, y, options) {
@@ -60,8 +42,11 @@ export class DocumentUtil {
const {deepContentScan, normalizeCssZoom} = options;
const elements = this._getElementsFromPoint(x, y, deepContentScan);
+ /** @type {?HTMLDivElement} */
let imposter = null;
+ /** @type {?HTMLDivElement} */
let imposterContainer = null;
+ /** @type {?Element} */
let imposterSourceElement = null;
if (elements.length > 0) {
const element = elements[0];
@@ -71,14 +56,14 @@ export class DocumentUtil {
case 'SELECT':
return TextSourceElement.create(element);
case 'INPUT':
- if (element.type === 'text') {
+ if (/** @type {HTMLInputElement} */ (element).type === 'text') {
imposterSourceElement = element;
- [imposter, imposterContainer] = this._createImposter(element, false);
+ [imposter, imposterContainer] = this._createImposter(/** @type {HTMLInputElement} */ (element), false);
}
break;
case 'TEXTAREA':
imposterSourceElement = element;
- [imposter, imposterContainer] = this._createImposter(element, true);
+ [imposter, imposterContainer] = this._createImposter(/** @type {HTMLTextAreaElement} */ (element), true);
break;
}
}
@@ -86,14 +71,17 @@ export class DocumentUtil {
const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : [], normalizeCssZoom);
if (range !== null) {
if (imposter !== null) {
- this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
+ this._setImposterStyle(/** @type {HTMLDivElement} */ (imposterContainer).style, 'z-index', '-2147483646');
this._setImposterStyle(imposter.style, 'pointer-events', 'none');
- return TextSourceRange.createFromImposter(range, imposterContainer, imposterSourceElement);
+ return TextSourceRange.createFromImposter(range, /** @type {HTMLDivElement} */ (imposterContainer), /** @type {HTMLElement} */ (imposterSourceElement));
}
return TextSourceRange.create(range);
} else {
if (imposterContainer !== null) {
- imposterContainer.parentNode.removeChild(imposterContainer);
+ const {parentNode} = imposterContainer;
+ if (parentNode !== null) {
+ parentNode.removeChild(imposterContainer);
+ }
}
return null;
}
@@ -101,7 +89,7 @@ export class DocumentUtil {
/**
* Registers a custom handler for scanning for text or elements at the input position.
- * @param {GetRangeFromPointHandler} handler The handler callback which will be invoked when calling `getRangeFromPoint`.
+ * @param {import('document-util').GetRangeFromPointHandler} handler The handler callback which will be invoked when calling `getRangeFromPoint`.
*/
static registerGetRangeFromPointHandler(handler) {
this._getRangeFromPointHandlers.push(handler);
@@ -129,7 +117,7 @@ export class DocumentUtil {
* ```js
* new Map([ [character: string, [otherCharacter: string, includeCharacterAtEnd: boolean]], ... ])
* ```
- * @returns {{sentence: string, offset: number}} The sentence and the offset to the original source.
+ * @returns {{text: string, offset: number}} The sentence and the offset to the original source.
*/
static extractSentence(source, layoutAwareScan, extent, terminateAtNewlines, terminatorMap, forwardQuoteMap, backwardQuoteMap) {
// Scan text
@@ -218,11 +206,12 @@ export 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.
+ * @param {?Node} node A node in the document.
* @returns {number} The scaling factor.
*/
static computeZoomScale(node) {
if (this._cssZoomSupported === null) {
+ // @ts-ignore - zoom is a non-standard property that exists in Chromium-based browsers
this._cssZoomSupported = (typeof document.createElement('div').style.zoom === 'string');
}
if (!this._cssZoomSupported) { return 1; }
@@ -237,7 +226,7 @@ export class DocumentUtil {
for (; node !== null && node !== documentElement; node = node.parentNode) {
const {nodeType} = node;
if (nodeType === DOCUMENT_FRAGMENT_NODE) {
- const {host} = node;
+ const {host} = /** @type {ShadowRoot} */ (node);
if (typeof host !== 'undefined') {
node = host;
}
@@ -245,9 +234,9 @@ export class DocumentUtil {
} else if (nodeType !== ELEMENT_NODE) {
continue;
}
- let {zoom} = getComputedStyle(node);
- if (typeof zoom !== 'string') { continue; }
- zoom = Number.parseFloat(zoom);
+ const zoomString = getComputedStyle(/** @type {HTMLElement} */ (node)).getPropertyValue('zoom');
+ if (typeof zoomString !== 'string' || zoomString.length === 0) { continue; }
+ const zoom = Number.parseFloat(zoomString);
if (!Number.isFinite(zoom) || zoom === 0) { continue; }
scale *= zoom;
}
@@ -267,13 +256,13 @@ export class DocumentUtil {
/**
* Converts multiple rects based on the CSS zoom scaling for a given node.
- * @param {DOMRect[]} rects The rects to convert.
+ * @param {DOMRect[]|DOMRectList} rects The rects to convert.
* @param {Node} node The node to compute the zoom from.
* @returns {DOMRect[]} The updated rects, or the same rects array if no change is needed.
*/
static convertMultipleRectZoomCoordinates(rects, node) {
const scale = this.computeZoomScale(node);
- if (scale === 1) { return rects; }
+ 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));
@@ -299,7 +288,7 @@ export class DocumentUtil {
* Checks whether a given point is contained within any rect in a list.
* @param {number} x The horizontal coordinate.
* @param {number} y The vertical coordinate.
- * @param {DOMRect[]} rects The rect to check.
+ * @param {DOMRect[]|DOMRectList} rects The rect to check.
* @returns {boolean} `true` if the point is inside any of the rects, `false` otherwise.
*/
static isPointInAnyRect(x, y, rects) {
@@ -331,9 +320,10 @@ export class DocumentUtil {
/**
* Gets an array of the active modifier keys.
* @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check.
- * @returns {string[]} An array of modifiers.
+ * @returns {import('input').ModifierKey[]} An array of modifiers.
*/
static getActiveModifiers(event) {
+ /** @type {import('input').ModifierKey[]} */
const modifiers = [];
if (event.altKey) { modifiers.push('alt'); }
if (event.ctrlKey) { modifiers.push('ctrl'); }
@@ -345,20 +335,24 @@ export class DocumentUtil {
/**
* Gets an array of the active modifier keys and buttons.
* @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check.
- * @returns {string[]} An array of modifiers and buttons.
+ * @returns {import('input').Modifier[]} An array of modifiers and buttons.
*/
static getActiveModifiersAndButtons(event) {
+ /** @type {import('input').Modifier[]} */
const modifiers = this.getActiveModifiers(event);
- this._getActiveButtons(event, modifiers);
+ if (event instanceof MouseEvent) {
+ this._getActiveButtons(event, modifiers);
+ }
return modifiers;
}
/**
* Gets an array of the active buttons.
- * @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check.
- * @returns {string[]} An array of modifiers and buttons.
+ * @param {MouseEvent} event The event to check.
+ * @returns {import('input').ModifierMouseButton[]} An array of modifiers and buttons.
*/
static getActiveButtons(event) {
+ /** @type {import('input').ModifierMouseButton[]} */
const buttons = [];
this._getActiveButtons(event, buttons);
return buttons;
@@ -366,7 +360,7 @@ export class DocumentUtil {
/**
* Adds a fullscreen change event listener. This function handles all of the browser-specific variants.
- * @param {Function} onFullscreenChanged The event callback.
+ * @param {EventListener} onFullscreenChanged The event callback.
* @param {?EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to.
*/
static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) {
@@ -394,8 +388,11 @@ export class DocumentUtil {
static getFullscreenElement() {
return (
document.fullscreenElement ||
+ // @ts-ignore - vendor prefix
document.msFullscreenElement ||
+ // @ts-ignore - vendor prefix
document.mozFullScreenElement ||
+ // @ts-ignore - vendor prefix
document.webkitFullscreenElement ||
null
);
@@ -409,7 +406,7 @@ export class DocumentUtil {
static getNodesInRange(range) {
const end = range.endContainer;
const nodes = [];
- for (let node = range.startContainer; node !== null; node = this.getNextNode(node)) {
+ for (let node = /** @type {?Node} */ (range.startContainer); node !== null; node = this.getNextNode(node)) {
nodes.push(node);
if (node === end) { break; }
}
@@ -422,7 +419,7 @@ export class DocumentUtil {
* @returns {?Node} The next node, or `null` if there is no next node.
*/
static getNextNode(node) {
- let next = node.firstChild;
+ let next = /** @type {?Node} */ (node.firstChild);
if (next === null) {
while (true) {
next = node.nextSibling;
@@ -445,10 +442,14 @@ export class DocumentUtil {
*/
static anyNodeMatchesSelector(nodes, selector) {
const ELEMENT_NODE = Node.ELEMENT_NODE;
- for (let node of nodes) {
- for (; node !== null; node = node.parentNode) {
- if (node.nodeType !== ELEMENT_NODE) { continue; }
- if (node.matches(selector)) { return true; }
+ // This is a rather ugly way of getting the "node" variable to be a nullable
+ for (let node of /** @type {(?Node)[]} */ (nodes)) {
+ while (node !== null) {
+ if (node.nodeType !== ELEMENT_NODE) {
+ node = node.parentNode;
+ continue;
+ }
+ if (/** @type {HTMLElement} */ (node).matches(selector)) { return true; }
break;
}
}
@@ -463,10 +464,11 @@ export class DocumentUtil {
*/
static everyNodeMatchesSelector(nodes, selector) {
const ELEMENT_NODE = Node.ELEMENT_NODE;
- for (let node of nodes) {
+ // This is a rather ugly way of getting the "node" variable to be a nullable
+ for (let node of /** @type {(?Node)[]} */ (nodes)) {
while (true) {
if (node === null) { return false; }
- if (node.nodeType === ELEMENT_NODE && node.matches(selector)) { break; }
+ if (node.nodeType === ELEMENT_NODE && /** @type {HTMLElement} */ (node).matches(selector)) { break; }
node = node.parentNode;
}
}
@@ -497,7 +499,7 @@ export class DocumentUtil {
case 'SELECT':
return true;
default:
- return element.isContentEditable;
+ return element instanceof HTMLElement && element.isContentEditable;
}
}
@@ -506,7 +508,7 @@ export class DocumentUtil {
* @param {DOMRect[]} rects The DOMRects to offset.
* @param {number} x The horizontal offset amount.
* @param {number} y The vertical offset amount.
- * @returns {DOMRect} The DOMRects with the offset applied.
+ * @returns {DOMRect[]} The DOMRects with the offset applied.
*/
static offsetDOMRects(rects, x, y) {
const results = [];
@@ -519,8 +521,8 @@ export class DocumentUtil {
/**
* Gets the parent writing mode of an element.
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode.
- * @param {Element} element The HTML element to check.
- * @returns {string} The writing mode.
+ * @param {?Element} element The HTML element to check.
+ * @returns {import('document-util').NormalizedWritingMode} The writing mode.
*/
static getElementWritingMode(element) {
if (element !== null) {
@@ -536,52 +538,95 @@ export class DocumentUtil {
* Normalizes a CSS writing mode value by converting non-standard and deprecated values
* into their corresponding standard vaules.
* @param {string} writingMode The writing mode to normalize.
- * @returns {string} The normalized writing mode.
+ * @returns {import('document-util').NormalizedWritingMode} The normalized writing mode.
*/
static normalizeWritingMode(writingMode) {
switch (writingMode) {
- case 'lr':
- case 'lr-tb':
- case 'rl':
- return 'horizontal-tb';
case 'tb':
return 'vertical-lr';
case 'tb-rl':
return 'vertical-rl';
- default:
+ case 'horizontal-tb':
+ case 'vertical-rl':
+ case 'vertical-lr':
+ case 'sideways-rl':
+ case 'sideways-lr':
return writingMode;
+ default: // 'lr', 'lr-tb', 'rl'
+ return 'horizontal-tb';
}
}
/**
* Converts a value from an element to a number.
- * @param {string} value A string representation of a number.
- * @param {object} constraints An object which might contain `min`, `max`, and `step` fields which are used to constrain the value.
+ * @param {string} valueString A string representation of a number.
+ * @param {import('document-util').ToNumberConstraints} constraints An object which might contain `min`, `max`, and `step` fields which are used to constrain the value.
* @returns {number} The parsed and constrained number.
*/
- static convertElementValueToNumber(value, constraints) {
- value = parseFloat(value);
+ static convertElementValueToNumber(valueString, constraints) {
+ let value = Number.parseFloat(valueString);
if (!Number.isFinite(value)) { value = 0; }
- let {min, max, step} = constraints;
- min = this._convertToNumberOrNull(min);
- max = this._convertToNumberOrNull(max);
- step = this._convertToNumberOrNull(step);
+ const min = this._convertToNumberOrNull(constraints.min);
+ const max = this._convertToNumberOrNull(constraints.max);
+ const step = this._convertToNumberOrNull(constraints.step);
if (typeof min === 'number') { value = Math.max(value, min); }
if (typeof max === 'number') { value = Math.min(value, max); }
if (typeof step === 'number' && step !== 0) { value = Math.round(value / step) * step; }
return value;
}
+ /**
+ * @param {string} value
+ * @returns {?import('input').Modifier}
+ */
+ static normalizeModifier(value) {
+ switch (value) {
+ case 'alt':
+ case 'ctrl':
+ case 'meta':
+ case 'shift':
+ case 'mouse0':
+ case 'mouse1':
+ case 'mouse2':
+ case 'mouse3':
+ case 'mouse4':
+ case 'mouse5':
+ return value;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @param {string} value
+ * @returns {?import('input').ModifierKey}
+ */
+ static normalizeModifierKey(value) {
+ switch (value) {
+ case 'alt':
+ case 'ctrl':
+ case 'meta':
+ case 'shift':
+ return value;
+ default:
+ return null;
+ }
+ }
+
// Private
+ /**
+ * @param {MouseEvent} event The event to check.
+ * @param {import('input').ModifierMouseButton[]|import('input').Modifier[]} array
+ */
static _getActiveButtons(event, array) {
let {buttons} = event;
if (typeof buttons === 'number' && buttons > 0) {
for (let i = 0; i < 6; ++i) {
const buttonFlag = (1 << i);
if ((buttons & buttonFlag) !== 0) {
- array.push(`mouse${i}`);
+ array.push(/** @type {import('input').ModifierMouseButton} */ (`mouse${i}`));
buttons &= ~buttonFlag;
if (buttons === 0) { break; }
}
@@ -589,10 +634,20 @@ export class DocumentUtil {
}
}
+ /**
+ * @param {CSSStyleDeclaration} style
+ * @param {string} propertyName
+ * @param {string} value
+ */
static _setImposterStyle(style, propertyName, value) {
style.setProperty(propertyName, value, 'important');
}
+ /**
+ * @param {HTMLInputElement|HTMLTextAreaElement} element
+ * @param {boolean} isTextarea
+ * @returns {[imposter: ?HTMLDivElement, container: ?HTMLDivElement]}
+ */
static _createImposter(element, isTextarea) {
const body = document.body;
if (body === null) { return [null, null]; }
@@ -669,6 +724,12 @@ export class DocumentUtil {
return [imposter, container];
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {boolean} all
+ * @returns {Element[]}
+ */
static _getElementsFromPoint(x, y, all) {
if (all) {
// document.elementsFromPoint can return duplicates which must be removed.
@@ -680,6 +741,13 @@ export class DocumentUtil {
return e !== null ? [e] : [];
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {Range} range
+ * @param {boolean} normalizeCssZoom
+ * @returns {boolean}
+ */
static _isPointInRange(x, y, range, normalizeCssZoom) {
// Require a text node to start
const {startContainer} = range;
@@ -722,16 +790,26 @@ export class DocumentUtil {
return false;
}
+ /**
+ * @param {string} string
+ * @returns {boolean}
+ */
static _isWhitespace(string) {
return string.trim().length === 0;
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @returns {?Range}
+ */
static _caretRangeFromPoint(x, y) {
if (typeof document.caretRangeFromPoint === 'function') {
// Chrome, Edge
return document.caretRangeFromPoint(x, y);
}
+ // @ts-ignore - caretPositionFromPoint is non-standard
if (typeof document.caretPositionFromPoint === 'function') {
// Firefox
return this._caretPositionFromPoint(x, y);
@@ -741,8 +819,14 @@ export class DocumentUtil {
return null;
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @returns {?Range}
+ */
static _caretPositionFromPoint(x, y) {
- const position = document.caretPositionFromPoint(x, y);
+ // @ts-ignore - caretPositionFromPoint is non-standard
+ const position = /** @type {(x: number, y: number) => ?{offsetNode: Node, offset: number}} */ (document.caretPositionFromPoint)(x, y);
if (position === null) {
return null;
}
@@ -760,8 +844,8 @@ export class DocumentUtil {
case Node.ELEMENT_NODE:
// Elements with user-select: all will return the element
// instead of a text point inside the element.
- if (this._isElementUserSelectAll(node)) {
- return this._caretPositionFromPointNormalizeStyles(x, y, node);
+ if (this._isElementUserSelectAll(/** @type {Element} */ (node))) {
+ return this._caretPositionFromPointNormalizeStyles(x, y, /** @type {Element} */ (node));
}
break;
}
@@ -778,14 +862,23 @@ export class DocumentUtil {
}
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {Element} nextElement
+ * @returns {?Range}
+ */
static _caretPositionFromPointNormalizeStyles(x, y, nextElement) {
const previousStyles = new Map();
try {
while (true) {
- this._recordPreviousStyle(previousStyles, nextElement);
- nextElement.style.setProperty('user-select', 'text', 'important');
+ if (nextElement instanceof HTMLElement) {
+ this._recordPreviousStyle(previousStyles, nextElement);
+ nextElement.style.setProperty('user-select', 'text', 'important');
+ }
- const position = document.caretPositionFromPoint(x, y);
+ // @ts-ignore - caretPositionFromPoint is non-standard
+ const position = /** @type {(x: number, y: number) => ?{offsetNode: Node, offset: number}} */ (document.caretPositionFromPoint)(x, y);
if (position === null) {
return null;
}
@@ -803,12 +896,12 @@ export class DocumentUtil {
case Node.ELEMENT_NODE:
// Elements with user-select: all will return the element
// instead of a text point inside the element.
- if (this._isElementUserSelectAll(node)) {
+ if (this._isElementUserSelectAll(/** @type {Element} */ (node))) {
if (previousStyles.has(node)) {
// Recursive
return null;
}
- nextElement = node;
+ nextElement = /** @type {Element} */ (node);
continue;
}
break;
@@ -830,6 +923,13 @@ export class DocumentUtil {
}
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {Element[]} elements
+ * @param {boolean} normalizeCssZoom
+ * @returns {?Range}
+ */
static _caretRangeFromPointExt(x, y, elements, normalizeCssZoom) {
let previousStyles = null;
try {
@@ -862,6 +962,12 @@ export class DocumentUtil {
}
}
+ /**
+ * @param {Element[]} elements
+ * @param {number} i
+ * @param {Map<Element, ?string>} previousStyles
+ * @returns {number}
+ */
static _disableTransparentElement(elements, i, previousStyles) {
while (true) {
if (i >= elements.length) {
@@ -870,19 +976,28 @@ export class DocumentUtil {
const element = elements[i++];
if (this._isElementTransparent(element)) {
- this._recordPreviousStyle(previousStyles, element);
- element.style.setProperty('pointer-events', 'none', 'important');
+ if (element instanceof HTMLElement) {
+ this._recordPreviousStyle(previousStyles, element);
+ element.style.setProperty('pointer-events', 'none', 'important');
+ }
return i;
}
}
}
+ /**
+ * @param {Map<Element, ?string>} previousStyles
+ * @param {Element} element
+ */
static _recordPreviousStyle(previousStyles, element) {
if (previousStyles.has(element)) { return; }
const style = element.hasAttribute('style') ? element.getAttribute('style') : null;
previousStyles.set(element, style);
}
+ /**
+ * @param {Map<Element, ?string>} previousStyles
+ */
static _revertStyles(previousStyles) {
for (const [element, style] of previousStyles.entries()) {
if (style === null) {
@@ -893,6 +1008,10 @@ export class DocumentUtil {
}
}
+ /**
+ * @param {Element} element
+ * @returns {boolean}
+ */
static _isElementTransparent(element) {
if (
element === document.body ||
@@ -908,14 +1027,26 @@ export class DocumentUtil {
);
}
+ /**
+ * @param {string} cssColor
+ * @returns {boolean}
+ */
static _isColorTransparent(cssColor) {
return this._transparentColorPattern.test(cssColor);
}
+ /**
+ * @param {Element} element
+ * @returns {boolean}
+ */
static _isElementUserSelectAll(element) {
return getComputedStyle(element).userSelect === 'all';
}
+ /**
+ * @param {string|number|undefined} value
+ * @returns {?number}
+ */
static _convertToNumberOrNull(value) {
if (typeof value !== 'number') {
if (typeof value !== 'string' || value.length === 0) {
@@ -926,9 +1057,12 @@ export class DocumentUtil {
return !Number.isNaN(value) ? value : null;
}
}
+/** @type {RegExp} */
// eslint-disable-next-line no-underscore-dangle
DocumentUtil._transparentColorPattern = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/;
+/** @type {?boolean} */
// eslint-disable-next-line no-underscore-dangle
DocumentUtil._cssZoomSupported = null;
+/** @type {import('document-util').GetRangeFromPointHandler[]} */
// eslint-disable-next-line no-underscore-dangle
DocumentUtil._getRangeFromPointHandlers = [];