551 lines
13 KiB
JavaScript
551 lines
13 KiB
JavaScript
/* 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;
|
|
}
|
|
}
|