Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// An autoselection smaller than these will be ignored entirely:
const MIN_DETECT_ABSOLUTE_HEIGHT = 10;
const MIN_DETECT_ABSOLUTE_WIDTH = 30;
// An autoselection smaller than these will not be preferred:
const MIN_DETECT_HEIGHT = 30;
const MIN_DETECT_WIDTH = 100;
// An autoselection bigger than either of these will be ignored:
let MAX_DETECT_HEIGHT = 700;
let MAX_DETECT_WIDTH = 1000;
const doNotAutoselectTags = {
H1: true,
H2: true,
H3: true,
H4: true,
H5: true,
H6: true,
};
/**
* Gets the rect for an element if getBoundingClientRect exists
* @param ele The element to get the rect from
* @returns The bounding client rect of the element or null
*/
function getBoundingClientRect(ele) {
if (!ele.getBoundingClientRect) {
return null;
}
return ele.getBoundingClientRect();
}
export function setMaxDetectHeight(maxHeight) {
MAX_DETECT_HEIGHT = maxHeight;
}
export function setMaxDetectWidth(maxWidth) {
MAX_DETECT_WIDTH = maxWidth;
}
/**
* This function will try to get an element from a given point in the doc.
* This function is recursive because when sending a message to the
* ScreenshotsHelper, the ScreenshotsHelper will call into this function.
* This only occurs when the element at the given point is an iframe.
*
* If the element is an iframe, we will send a message to the ScreenshotsHelper
* actor in the correct context to get the element at the given point.
* The message will return the "getBestRectForElement" for the element at the
* given point.
*
* If the element is not an iframe, then we will just return the element.
*
* @param {Number} x The x coordinate
* @param {Number} y The y coordinate
* @param {Document} doc The document
* @returns {Object}
* ele: The element for a given point (x, y)
* rect: The rect for the given point if ele is an iframe
* otherwise null
*/
export async function getElementFromPoint(x, y, doc) {
let ele = null;
let rect = null;
try {
ele = doc.elementFromPoint(x, y);
// if the element is an iframe, we need to send a message to that browsing context
// to get the coordinates of the element in the iframe
if (doc.defaultView.HTMLIFrameElement.isInstance(ele)) {
let actor =
ele.browsingContext.parentWindowContext.windowGlobalChild.getActor(
"ScreenshotsHelper"
);
rect = await actor.sendQuery(
"ScreenshotsHelper:GetElementRectFromPoint",
{
x: x + ele.ownerGlobal.mozInnerScreenX,
y: y + ele.ownerGlobal.mozInnerScreenY,
bcId: ele.browsingContext.id,
}
);
if (rect) {
rect = {
left: rect.left - ele.ownerGlobal.mozInnerScreenX,
right: rect.right - ele.ownerGlobal.mozInnerScreenX,
top: rect.top - ele.ownerGlobal.mozInnerScreenY,
bottom: rect.bottom - ele.ownerGlobal.mozInnerScreenY,
};
}
} else if (ele.openOrClosedShadowRoot) {
while (ele.openOrClosedShadowRoot) {
let shadowEle = ele.openOrClosedShadowRoot.elementFromPoint(x, y);
if (shadowEle) {
ele = shadowEle;
} else {
break;
}
}
}
} catch (e) {
console.error(e);
}
return { ele, rect };
}
/**
* This function takes an element and finds a suitable rect to draw the hover box on
* @param {Element} ele The element to find a suitale rect of
* @param {Document} doc The current document
* @returns A suitable rect or null
*/
export function getBestRectForElement(ele, doc) {
let lastRect;
let lastNode;
let rect;
let attemptExtend = false;
let node = ele;
while (node) {
rect = getBoundingClientRect(node);
if (!rect) {
rect = lastRect;
break;
}
if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) {
// Avoid infinite loop for elements with zero or nearly zero height,
// like non-clearfixed float parents with or without borders.
break;
}
if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) {
// Then the last rectangle is better
rect = lastRect;
attemptExtend = true;
break;
}
if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) {
if (!doNotAutoselectTags[node.tagName]) {
break;
}
}
lastRect = rect;
lastNode = node;
node = node.parentNode;
}
if (rect && node) {
const evenBetter = evenBetterElement(node, doc);
if (evenBetter) {
node = lastNode = evenBetter;
rect = getBoundingClientRect(evenBetter);
attemptExtend = false;
}
}
if (rect && attemptExtend) {
let extendNode = lastNode.nextSibling;
while (extendNode) {
if (extendNode.nodeType === doc.ELEMENT_NODE) {
break;
}
extendNode = extendNode.nextSibling;
if (!extendNode) {
const parentNode = lastNode.parentNode;
for (let i = 0; i < parentNode.childNodes.length; i++) {
if (parentNode.childNodes[i] === lastNode) {
extendNode = parentNode.childNodes[i + 1];
}
}
}
}
if (extendNode) {
const extendRect = getBoundingClientRect(extendNode);
let x = Math.min(rect.x, extendRect.x);
let y = Math.min(rect.y, extendRect.y);
let width = Math.max(rect.right, extendRect.right) - x;
let height = Math.max(rect.bottom, extendRect.bottom) - y;
const combinedRect = new DOMRect(x, y, width, height);
if (
combinedRect.width <= MAX_DETECT_WIDTH &&
combinedRect.height <= MAX_DETECT_HEIGHT
) {
rect = combinedRect;
}
}
}
if (
rect &&
(rect.width < MIN_DETECT_ABSOLUTE_WIDTH ||
rect.height < MIN_DETECT_ABSOLUTE_HEIGHT)
) {
rect = null;
}
return rect;
}
/**
* This finds a better element by looking for elements with role article
* @param {Element} node The currently hovered node
* @param {Document} doc The current document
* @returns A better node or null
*/
function evenBetterElement(node, doc) {
let el = node.parentNode;
const ELEMENT_NODE = doc.ELEMENT_NODE;
while (el && el.nodeType === ELEMENT_NODE) {
if (!el.getAttribute) {
return null;
}
if (el.getAttribute("role") === "article") {
const rect = getBoundingClientRect(el);
if (!rect) {
return null;
}
if (rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT) {
return el;
}
return null;
}
el = el.parentNode;
}
return null;
}
export class Region {
#x1;
#x2;
#y1;
#y2;
#xOffset;
#yOffset;
#windowDimensions;
constructor(windowDimensions) {
this.resetDimensions();
this.#windowDimensions = windowDimensions;
}
/**
* Sets the dimensions if the given dimension is defined.
* Otherwise will reset the dimensions
* @param {Object} dims The new region dimensions
* {
* left: new left dimension value or undefined
* top: new top dimension value or undefined
* right: new right dimension value or undefined
* bottom: new bottom dimension value or undefined
* }
*/
set dimensions(dims) {
if (dims == null) {
this.resetDimensions();
return;
}
if (dims.left != null) {
this.left = dims.left;
}
if (dims.top != null) {
this.top = dims.top;
}
if (dims.right != null) {
this.right = dims.right;
}
if (dims.bottom != null) {
this.bottom = dims.bottom;
}
}
get dimensions() {
return {
left: this.left,
top: this.top,
right: this.right,
bottom: this.bottom,
width: this.width,
height: this.height,
};
}
get isRegionValid() {
return this.#x1 + this.#x2 + this.#y1 + this.#y2 > 0;
}
resetDimensions() {
this.#x1 = 0;
this.#x2 = 0;
this.#y1 = 0;
this.#y2 = 0;
this.#xOffset = 0;
this.#yOffset = 0;
}
/**
* Sort the coordinates so x1 < x2 and y1 < y2
*/
sortCoords() {
if (this.#x1 > this.#x2) {
[this.#x1, this.#x2] = [this.#x2, this.#x1];
}
if (this.#y1 > this.#y2) {
[this.#y1, this.#y2] = [this.#y2, this.#y1];
}
}
/**
* The region should never appear outside the document so the region will
* be shifted if the region is outside the page's width or height.
*/
shift() {
let didShift = false;
let xDiff = this.right - this.#windowDimensions.scrollWidth;
if (xDiff > 0) {
this.left -= xDiff;
this.right -= xDiff;
didShift = true;
}
let yDiff = this.bottom - this.#windowDimensions.scrollHeight;
if (yDiff > 0) {
this.top -= yDiff;
this.bottom -= yDiff;
didShift = true;
}
return didShift;
}
/**
* The diagonal distance of the region
*/
get distance() {
return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2));
}
get xOffset() {
return this.#xOffset;
}
set xOffset(val) {
this.#xOffset = val;
}
get yOffset() {
return this.#yOffset;
}
set yOffset(val) {
this.#yOffset = val;
}
get top() {
return Math.min(this.#y1, this.#y2);
}
set top(val) {
this.#y1 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
}
get left() {
return Math.min(this.#x1, this.#x2);
}
set left(val) {
this.#x1 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
}
get right() {
return Math.max(this.#x1, this.#x2);
}
set right(val) {
this.#x2 = Math.min(this.#windowDimensions.scrollWidth, Math.max(0, val));
}
get bottom() {
return Math.max(this.#y1, this.#y2);
}
set bottom(val) {
this.#y2 = Math.min(this.#windowDimensions.scrollHeight, Math.max(0, val));
}
get width() {
return Math.abs(this.#x2 - this.#x1);
}
get height() {
return Math.abs(this.#y2 - this.#y1);
}
get x1() {
return this.#x1;
}
get x2() {
return this.#x2;
}
get y1() {
return this.#y1;
}
get y2() {
return this.#y2;
}
}
export class WindowDimensions {
#clientHeight = null;
#clientWidth = null;
#scrollHeight = null;
#scrollWidth = null;
#scrollX = null;
#scrollY = null;
#scrollMinX = null;
#scrollMinY = null;
#scrollMaxX = null;
#scrollMaxY = null;
#devicePixelRatio = null;
set dimensions(dimensions) {
if (dimensions.clientHeight != null) {
this.#clientHeight = dimensions.clientHeight;
}
if (dimensions.clientWidth != null) {
this.#clientWidth = dimensions.clientWidth;
}
if (dimensions.scrollHeight != null) {
this.#scrollHeight = dimensions.scrollHeight;
}
if (dimensions.scrollWidth != null) {
this.#scrollWidth = dimensions.scrollWidth;
}
if (dimensions.scrollX != null) {
this.#scrollX = dimensions.scrollX;
}
if (dimensions.scrollY != null) {
this.#scrollY = dimensions.scrollY;
}
if (dimensions.scrollMinX != null) {
this.#scrollMinX = dimensions.scrollMinX;
}
if (dimensions.scrollMinY != null) {
this.#scrollMinY = dimensions.scrollMinY;
}
if (dimensions.scrollMaxX != null) {
this.#scrollMaxX = dimensions.scrollMaxX;
}
if (dimensions.scrollMaxY != null) {
this.#scrollMaxY = dimensions.scrollMaxY;
}
if (dimensions.devicePixelRatio != null) {
this.#devicePixelRatio = dimensions.devicePixelRatio;
}
}
get dimensions() {
return {
clientHeight: this.clientHeight,
clientWidth: this.clientWidth,
scrollHeight: this.scrollHeight,
scrollWidth: this.scrollWidth,
scrollX: this.scrollX,
scrollY: this.scrollY,
pageScrollX: this.pageScrollX,
pageScrollY: this.pageScrollY,
scrollMinX: this.scrollMinX,
scrollMinY: this.scrollMinY,
scrollMaxX: this.scrollMaxX,
scrollMaxY: this.scrollMaxY,
devicePixelRatio: this.devicePixelRatio,
};
}
get clientWidth() {
return this.#clientWidth;
}
get clientHeight() {
return this.#clientHeight;
}
get scrollWidth() {
return this.#scrollWidth;
}
get scrollHeight() {
return this.#scrollHeight;
}
get scrollX() {
return this.#scrollX - this.scrollMinX;
}
get pageScrollX() {
return this.#scrollX;
}
get scrollY() {
return this.#scrollY - this.scrollMinY;
}
get pageScrollY() {
return this.#scrollY;
}
get scrollMinX() {
return this.#scrollMinX;
}
get scrollMinY() {
return this.#scrollMinY;
}
get scrollMaxX() {
return this.#scrollMaxX;
}
get scrollMaxY() {
return this.#scrollMaxY;
}
get devicePixelRatio() {
return this.#devicePixelRatio;
}
isInViewport(rect) {
// eslint-disable-next-line no-shadow
let { left, top, right, bottom } = rect;
if (
left > this.scrollX + this.clientWidth ||
right < this.scrollX ||
top > this.scrollY + this.clientHeight ||
bottom < this.scrollY
) {
return false;
}
return true;
}
reset() {
this.#clientHeight = 0;
this.#clientWidth = 0;
this.#scrollHeight = 0;
this.#scrollWidth = 0;
this.#scrollX = 0;
this.#scrollY = 0;
this.#scrollMinX = 0;
this.#scrollMinY = 0;
this.#scrollMaxX = 0;
this.#scrollMaxY = 0;
}
}