diff options
Diffstat (limited to 'browser/components/screenshots/overlayHelpers.mjs')
-rw-r--r-- | browser/components/screenshots/overlayHelpers.mjs | 497 |
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; + } +} |