summaryrefslogtreecommitdiffstats
path: root/browser/components/screenshots/overlayHelpers.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/screenshots/overlayHelpers.mjs')
-rw-r--r--browser/components/screenshots/overlayHelpers.mjs497
1 files changed, 497 insertions, 0 deletions
diff --git a/browser/components/screenshots/overlayHelpers.mjs b/browser/components/screenshots/overlayHelpers.mjs
new file mode 100644
index 0000000000..70a1bd86d0
--- /dev/null
+++ b/browser/components/screenshots/overlayHelpers.mjs
@@ -0,0 +1,497 @@
+/* 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,
+ };
+ }
+ }
+ } 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;
+ #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.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,
+ scrollMinX: this.#scrollMinX,
+ scrollMinY: this.#scrollMinY,
+ 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;
+ }
+
+ get scrollY() {
+ return this.#scrollY;
+ }
+
+ get scrollMinX() {
+ return this.#scrollMinX;
+ }
+
+ get scrollMinY() {
+ return this.#scrollMinY;
+ }
+
+ get devicePixelRatio() {
+ return this.#devicePixelRatio;
+ }
+
+ 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;
+ }
+}