diff options
Diffstat (limited to 'devtools/shared/layout')
-rw-r--r-- | devtools/shared/layout/dom-matrix-2d.js | 297 | ||||
-rw-r--r-- | devtools/shared/layout/moz.build | 7 | ||||
-rw-r--r-- | devtools/shared/layout/utils.js | 930 |
3 files changed, 1234 insertions, 0 deletions
diff --git a/devtools/shared/layout/dom-matrix-2d.js b/devtools/shared/layout/dom-matrix-2d.js new file mode 100644 index 0000000000..f6e3e73067 --- /dev/null +++ b/devtools/shared/layout/dom-matrix-2d.js @@ -0,0 +1,297 @@ +/* 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/. */ + +"use strict"; + +/** + * Returns a matrix for the scaling given. + * Calling `scale()` or `scale(1) returns a new identity matrix. + * + * @param {Number} [sx = 1] + * the abscissa of the scaling vector. + * If unspecified, it will equal to `1`. + * @param {Number} [sy = sx] + * The ordinate of the scaling vector. + * If not present, its default value is `sx`, leading to a uniform scaling. + * @return {Array} + * The new matrix. + */ +const scale = (sx = 1, sy = sx) => [sx, 0, 0, 0, sy, 0, 0, 0, 1]; +exports.scale = scale; + +/** + * Returns a matrix for the translation given. + * Calling `translate()` or `translate(0) returns a new identity matrix. + * + * @param {Number} [tx = 0] + * The abscissa of the translating vector. + * If unspecified, it will equal to `0`. + * @param {Number} [ty = tx] + * The ordinate of the translating vector. + * If unspecified, it will equal to `tx`. + * @return {Array} + * The new matrix. + */ +const translate = (tx = 0, ty = tx) => [1, 0, tx, 0, 1, ty, 0, 0, 1]; +exports.translate = translate; + +/** + * Returns a matrix that reflects about the Y axis. For example, the point (x1, y1) would + * become (-x1, y1). + * + * @return {Array} + * The new matrix. + */ +const reflectAboutY = () => [-1, 0, 0, 0, 1, 0, 0, 0, 1]; +exports.reflectAboutY = reflectAboutY; + +/** + * Returns a matrix for the rotation given. + * Calling `rotate()` or `rotate(0)` returns a new identity matrix. + * + * @param {Number} [angle = 0] + * The angle, in radians, for which to return a corresponding rotation matrix. + * If unspecified, it will equal `0`. + * @return {Array} + * The new matrix. + */ +const rotate = (angle = 0) => { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + return [cos, sin, 0, -sin, cos, 0, 0, 0, 1]; +}; +exports.rotate = rotate; + +/** + * Returns a new identity matrix. + * + * @return {Array} + * The new matrix. + */ +const identity = () => [1, 0, 0, 0, 1, 0, 0, 0, 1]; +exports.identity = identity; + +/** + * Multiplies two matrices and returns a new matrix with the result. + * + * @param {Array} M1 + * The first operand. + * @param {Array} M2 + * The second operand. + * @return {Array} + * The resulting matrix. + */ +const multiply = (M1, M2) => { + const c11 = M1[0] * M2[0] + M1[1] * M2[3] + M1[2] * M2[6]; + const c12 = M1[0] * M2[1] + M1[1] * M2[4] + M1[2] * M2[7]; + const c13 = M1[0] * M2[2] + M1[1] * M2[5] + M1[2] * M2[8]; + + const c21 = M1[3] * M2[0] + M1[4] * M2[3] + M1[5] * M2[6]; + const c22 = M1[3] * M2[1] + M1[4] * M2[4] + M1[5] * M2[7]; + const c23 = M1[3] * M2[2] + M1[4] * M2[5] + M1[5] * M2[8]; + + const c31 = M1[6] * M2[0] + M1[7] * M2[3] + M1[8] * M2[6]; + const c32 = M1[6] * M2[1] + M1[7] * M2[4] + M1[8] * M2[7]; + const c33 = M1[6] * M2[2] + M1[7] * M2[5] + M1[8] * M2[8]; + + return [c11, c12, c13, c21, c22, c23, c31, c32, c33]; +}; +exports.multiply = multiply; + +/** + * Applies the given matrix to a point. + * + * @param {Array} M + * The matrix to apply. + * @param {Array} P + * The point's vector. + * @return {Array} + * The resulting point's vector. + */ +const apply = (M, P) => [ + M[0] * P[0] + M[1] * P[1] + M[2], + M[3] * P[0] + M[4] * P[1] + M[5], +]; +exports.apply = apply; + +/** + * Returns `true` if the given matrix is a identity matrix. + * + * @param {Array} M + * The matrix to check + * @return {Boolean} + * `true` if the matrix passed is a identity matrix, `false` otherwise. + */ +const isIdentity = M => + M[0] === 1 && + M[1] === 0 && + M[2] === 0 && + M[3] === 0 && + M[4] === 1 && + M[5] === 0 && + M[6] === 0 && + M[7] === 0 && + M[8] === 1; +exports.isIdentity = isIdentity; + +/** + * Get the change of basis matrix and inverted change of basis matrix + * for the coordinate system based on the two given vectors, as well as + * the lengths of the two given vectors. + * + * @param {Array} u + * The first vector, serving as the "x axis" of the coordinate system. + * @param {Array} v + * The second vector, serving as the "y axis" of the coordinate system. + * @return {Object} + * { basis, invertedBasis, uLength, vLength } + * basis and invertedBasis are the change of basis matrices. uLength and + * vLength are the lengths of u and v. + */ +const getBasis = (u, v) => { + const uLength = Math.abs(Math.sqrt(u[0] ** 2 + u[1] ** 2)); + const vLength = Math.abs(Math.sqrt(v[0] ** 2 + v[1] ** 2)); + const basis = [ + u[0] / uLength, + v[0] / vLength, + 0, + u[1] / uLength, + v[1] / vLength, + 0, + 0, + 0, + 1, + ]; + const determinant = 1 / (basis[0] * basis[4] - basis[1] * basis[3]); + const invertedBasis = [ + basis[4] / determinant, + -basis[1] / determinant, + 0, + -basis[3] / determinant, + basis[0] / determinant, + 0, + 0, + 0, + 1, + ]; + return { basis, invertedBasis, uLength, vLength }; +}; +exports.getBasis = getBasis; + +/** + * Convert the given matrix to a new coordinate system, based on the change of basis + * matrix. + * + * @param {Array} M + * The matrix to convert + * @param {Array} basis + * The change of basis matrix + * @param {Array} invertedBasis + * The inverted change of basis matrix + * @return {Array} + * The converted matrix. + */ +const changeMatrixBase = (M, basis, invertedBasis) => { + return multiply(invertedBasis, multiply(M, basis)); +}; +exports.changeMatrixBase = changeMatrixBase; + +/** + * Returns the transformation matrix for the given node, relative to the ancestor passed + * as second argument; considering the ancestor transformation too. + * If no ancestor is specified, it will returns the transformation matrix relative to the + * node's parent element. + * + * @param {DOMNode} node + * The node. + * @param {DOMNode} ancestor + * The ancestor of the node given. + * @return {Array} + * The transformation matrix. + */ +function getNodeTransformationMatrix(node, ancestor = node.parentElement) { + const { a, b, c, d, e, f } = ancestor + .getTransformToParent() + .multiply(node.getTransformToAncestor(ancestor)); + + return [a, c, e, b, d, f, 0, 0, 1]; +} +exports.getNodeTransformationMatrix = getNodeTransformationMatrix; + +/** + * Returns the matrix to rotate, translate, and reflect (if needed) from the element's + * top-left origin into the actual writing mode and text direction applied to the element. + * + * @param {Object} size + * An element's untransformed content `width` and `height` (excluding any margin, + * borders, or padding). + * @param {Object} style + * The computed `writingMode` and `direction` properties for the element. + * @return {Array} + * The matrix with adjustments for writing mode and text direction, if any. + */ +function getWritingModeMatrix(size, style) { + let currentMatrix = identity(); + const { width, height } = size; + const { direction, writingMode } = style; + + switch (writingMode) { + case "horizontal-tb": + // This is the initial value. No further adjustment needed. + break; + case "vertical-rl": + currentMatrix = multiply(translate(width, 0), rotate(-Math.PI / 2)); + break; + case "vertical-lr": + currentMatrix = multiply(reflectAboutY(), rotate(-Math.PI / 2)); + break; + case "sideways-rl": + currentMatrix = multiply(translate(width, 0), rotate(-Math.PI / 2)); + break; + case "sideways-lr": + currentMatrix = multiply(rotate(Math.PI / 2), translate(-height, 0)); + break; + default: + console.error(`Unexpected writing-mode: ${writingMode}`); + } + + switch (direction) { + case "ltr": + // This is the initial value. No further adjustment needed. + break; + case "rtl": + let rowLength = width; + if (writingMode != "horizontal-tb") { + rowLength = height; + } + currentMatrix = multiply(currentMatrix, translate(rowLength, 0)); + currentMatrix = multiply(currentMatrix, reflectAboutY()); + break; + default: + console.error(`Unexpected direction: ${direction}`); + } + + return currentMatrix; +} +exports.getWritingModeMatrix = getWritingModeMatrix; + +/** + * Convert from the matrix format used in this module: + * a, c, e, + * b, d, f, + * 0, 0, 1 + * to the format used by the `matrix()` CSS transform function: + * a, b, c, d, e, f + * + * @param {Array} M + * The matrix in this module's 9 element format. + * @return {String} + * The matching 6 element CSS transform function. + */ +function getCSSMatrixTransform(M) { + const [a, c, e, b, d, f] = M; + return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`; +} +exports.getCSSMatrixTransform = getCSSMatrixTransform; diff --git a/devtools/shared/layout/moz.build b/devtools/shared/layout/moz.build new file mode 100644 index 0000000000..da30931458 --- /dev/null +++ b/devtools/shared/layout/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules("dom-matrix-2d.js", "utils.js") diff --git a/devtools/shared/layout/utils.js b/devtools/shared/layout/utils.js new file mode 100644 index 0000000000..3e690fc5c3 --- /dev/null +++ b/devtools/shared/layout/utils.js @@ -0,0 +1,930 @@ +/* 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/. */ + +"use strict"; + +loader.lazyRequireGetter( + this, + "DevToolsUtils", + "resource://devtools/shared/DevToolsUtils.js" +); +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); + +const SHEET_TYPE = { + agent: "AGENT_SHEET", + user: "USER_SHEET", + author: "AUTHOR_SHEET", +}; + +// eslint-disable-next-line no-unused-vars +loader.lazyRequireGetter( + this, + "setIgnoreLayoutChanges", + "resource://devtools/server/actors/reflow.js", + true +); +exports.setIgnoreLayoutChanges = (...args) => + this.setIgnoreLayoutChanges(...args); + +/** + * Returns the `DOMWindowUtils` for the window given. + * + * @param {DOMWindow} win + * @returns {DOMWindowUtils} + */ +const utilsCache = new WeakMap(); +function utilsFor(win) { + // XXXbz Given that we now have a direct getter for the DOMWindowUtils, is + // this weakmap cache path any faster than just calling the getter? + if (!utilsCache.has(win)) { + utilsCache.set(win, win.windowUtils); + } + return utilsCache.get(win); +} + +/** + * Check a window is part of the boundary window given. + * + * @param {DOMWindow} boundaryWindow + * @param {DOMWindow} win + * @return {Boolean} + */ +function isWindowIncluded(boundaryWindow, win) { + if (win === boundaryWindow) { + return true; + } + + const parent = win.parent; + + if (!parent || parent === win) { + return false; + } + + return isWindowIncluded(boundaryWindow, parent); +} +exports.isWindowIncluded = isWindowIncluded; + +/** + * like win.frameElement, but goes through mozbrowsers and mozapps iframes. + * + * @param {DOMWindow} win + * The window to get the frame for + * @return {DOMNode} + * The element in which the window is embedded. + */ +const getFrameElement = win => { + const isTopWindow = win && DevToolsUtils.getTopWindow(win) === win; + return isTopWindow ? null : win.browsingContext.embedderElement; +}; +exports.getFrameElement = getFrameElement; + +/** + * Get the x/y offsets for of all the parent frames of a given node, limited to + * the boundary window given. + * + * @param {DOMWindow} boundaryWindow + * The window where to stop to iterate. If `null` is given, the top + * window is used. + * @param {DOMNode} node + * The node for which we are to get the offset + * @return {Array} + * The frame offset [x, y] + */ +function getFrameOffsets(boundaryWindow, node) { + let xOffset = 0; + let yOffset = 0; + + let frameWin = getWindowFor(node); + const scale = getCurrentZoom(node); + + if (boundaryWindow === null) { + boundaryWindow = DevToolsUtils.getTopWindow(frameWin); + } else if (typeof boundaryWindow === "undefined") { + throw new Error("No boundaryWindow given. Use null for the default one."); + } + + while (frameWin !== boundaryWindow) { + const frameElement = getFrameElement(frameWin); + if (!frameElement) { + break; + } + + // We are in an iframe. + // We take into account the parent iframe position and its + // offset (borders and padding). + const frameRect = frameElement.getBoundingClientRect(); + + const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement); + + xOffset += frameRect.left + offsetLeft; + yOffset += frameRect.top + offsetTop; + + frameWin = frameWin.parent; + } + + return [xOffset * scale, yOffset * scale]; +} +exports.getFrameOffsets = getFrameOffsets; + +/** + * Get box quads adjusted for iframes and zoom level. + * + * Warning: this function returns things that look like DOMQuad objects but + * aren't (they resemble an old version of the spec). Unlike the return value + * of node.getBoxQuads, they have a .bounds property and not a .getBounds() + * method. + * + * @param {DOMWindow} boundaryWindow + * The window where to stop to iterate. If `null` is given, the top + * window is used. + * @param {DOMNode} node + * The node for which we are to get the box model region + * quads. + * @param {String} region + * The box model region to return: "content", "padding", "border" or + * "margin". + * @param {Object} [options.ignoreZoom=false] + * Ignore zoom used in the context of e.g. canvas. + * @return {Array} + * An array of objects that have the same structure as quads returned by + * getBoxQuads. An empty array if the node has no quads or is invalid. + */ +function getAdjustedQuads( + boundaryWindow, + node, + region, + { ignoreZoom, ignoreScroll } = {} +) { + if (!node || !node.getBoxQuads) { + return []; + } + + const quads = node.getBoxQuads({ + box: region, + relativeTo: boundaryWindow.document, + createFramesForSuppressedWhitespace: false, + }); + + if (!quads.length) { + return []; + } + + const scale = ignoreZoom ? 1 : getCurrentZoom(node); + const { scrollX, scrollY } = ignoreScroll + ? { scrollX: 0, scrollY: 0 } + : boundaryWindow; + + const xOffset = scrollX * scale; + const yOffset = scrollY * scale; + + const adjustedQuads = []; + for (const quad of quads) { + const bounds = quad.getBounds(); + adjustedQuads.push({ + p1: { + w: quad.p1.w * scale, + x: quad.p1.x * scale + xOffset, + y: quad.p1.y * scale + yOffset, + z: quad.p1.z * scale, + }, + p2: { + w: quad.p2.w * scale, + x: quad.p2.x * scale + xOffset, + y: quad.p2.y * scale + yOffset, + z: quad.p2.z * scale, + }, + p3: { + w: quad.p3.w * scale, + x: quad.p3.x * scale + xOffset, + y: quad.p3.y * scale + yOffset, + z: quad.p3.z * scale, + }, + p4: { + w: quad.p4.w * scale, + x: quad.p4.x * scale + xOffset, + y: quad.p4.y * scale + yOffset, + z: quad.p4.z * scale, + }, + bounds: { + bottom: bounds.bottom * scale + yOffset, + height: bounds.height * scale, + left: bounds.left * scale + xOffset, + right: bounds.right * scale + xOffset, + top: bounds.top * scale + yOffset, + width: bounds.width * scale, + x: bounds.x * scale + xOffset, + y: bounds.y * scale + yOffset, + }, + }); + } + + return adjustedQuads; +} +exports.getAdjustedQuads = getAdjustedQuads; + +/** + * Compute the absolute position and the dimensions of a node, relativalely + * to the root window. + + * @param {DOMWindow} boundaryWindow + * The window where to stop to iterate. If `null` is given, the top + * window is used. + * @param {DOMNode} node + * a DOM element to get the bounds for + * @param {DOMWindow} contentWindow + * the content window holding the node + * @return {Object} + * A rect object with the {top, left, width, height} properties + */ +function getRect(boundaryWindow, node, contentWindow) { + let frameWin = node.ownerDocument.defaultView; + const clientRect = node.getBoundingClientRect(); + + if (boundaryWindow === null) { + boundaryWindow = DevToolsUtils.getTopWindow(frameWin); + } else if (typeof boundaryWindow === "undefined") { + throw new Error("No boundaryWindow given. Use null for the default one."); + } + + // Go up in the tree of frames to determine the correct rectangle. + // clientRect is read-only, we need to be able to change properties. + const rect = { + top: clientRect.top + contentWindow.pageYOffset, + left: clientRect.left + contentWindow.pageXOffset, + width: clientRect.width, + height: clientRect.height, + }; + + // We iterate through all the parent windows. + while (frameWin !== boundaryWindow) { + const frameElement = getFrameElement(frameWin); + if (!frameElement) { + break; + } + + // We are in an iframe. + // We take into account the parent iframe position and its + // offset (borders and padding). + const frameRect = frameElement.getBoundingClientRect(); + + const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement); + + rect.top += frameRect.top + offsetTop; + rect.left += frameRect.left + offsetLeft; + + frameWin = frameWin.parent; + } + + return rect; +} +exports.getRect = getRect; + +/** + * Get the 4 bounding points for a node taking iframes into account. + * Note that for transformed nodes, this will return the untransformed bound. + * + * @param {DOMWindow} boundaryWindow + * The window where to stop to iterate. If `null` is given, the top + * window is used. + * @param {DOMNode} node + * @return {Object} + * An object with p1,p2,p3,p4 properties being {x,y} objects + */ +function getNodeBounds(boundaryWindow, node) { + if (!node) { + return null; + } + const { scrollX, scrollY } = boundaryWindow; + const scale = getCurrentZoom(node); + + // Find out the offset of the node in its current frame + let offsetLeft = 0; + let offsetTop = 0; + let el = node; + while (el?.parentNode) { + offsetLeft += el.offsetLeft; + offsetTop += el.offsetTop; + el = el.offsetParent; + } + + // Also take scrolled containers into account + el = node; + while (el?.parentNode) { + if (el.scrollTop) { + offsetTop -= el.scrollTop; + } + if (el.scrollLeft) { + offsetLeft -= el.scrollLeft; + } + el = el.parentNode; + } + + // And add the potential frame offset if the node is nested + let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node); + xOffset += (offsetLeft + scrollX) * scale; + yOffset += (offsetTop + scrollY) * scale; + + // Get the width and height + const width = node.offsetWidth * scale; + const height = node.offsetHeight * scale; + + return { + p1: { x: xOffset, y: yOffset }, + p2: { x: xOffset + width, y: yOffset }, + p3: { x: xOffset + width, y: yOffset + height }, + p4: { x: xOffset, y: yOffset + height }, + top: yOffset, + right: xOffset + width, + bottom: yOffset + height, + left: xOffset, + width, + height, + }; +} +exports.getNodeBounds = getNodeBounds; + +/** + * Same as doing iframe.contentWindow but works with all types of container + * elements that act like frames (e.g. <embed>), where 'contentWindow' isn't a + * property that can be accessed. + * This uses the inIDeepTreeWalker instead. + * @param {DOMNode} frame + * @return {Window} + */ +function safelyGetContentWindow(frame) { + if (frame.contentWindow) { + return frame.contentWindow; + } + + const walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance( + Ci.inIDeepTreeWalker + ); + walker.showSubDocuments = true; + walker.showDocumentsAsNodes = true; + walker.init(frame); + walker.currentNode = frame; + + const document = walker.nextNode(); + if (!document || !document.defaultView) { + throw new Error("Couldn't get the content window inside frame " + frame); + } + + return document.defaultView; +} + +/** + * Returns a frame's content offset (frame border + padding). + * Note: this function shouldn't need to exist, had the platform provided a + * suitable API for determining the offset between the frame's content and + * its bounding client rect. Bug 626359 should provide us with such an API. + * + * @param {DOMNode} frame + * The frame. + * @return {Array} [offsetTop, offsetLeft] + * offsetTop is the distance from the top of the frame and the top of + * the content document. + * offsetLeft is the distance from the left of the frame and the left + * of the content document. + */ +function getFrameContentOffset(frame) { + const style = safelyGetContentWindow(frame).getComputedStyle(frame); + + // In some cases, the computed style is null + if (!style) { + return [0, 0]; + } + + const paddingTop = parseInt(style.getPropertyValue("padding-top"), 10); + const paddingLeft = parseInt(style.getPropertyValue("padding-left"), 10); + + const borderTop = parseInt(style.getPropertyValue("border-top-width"), 10); + const borderLeft = parseInt(style.getPropertyValue("border-left-width"), 10); + + return [borderTop + paddingTop, borderLeft + paddingLeft]; +} + +/** + * Check if a node and its document are still alive + * and attached to the window. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isNodeConnected(node) { + if (!node.ownerDocument || !node.ownerDocument.defaultView) { + return false; + } + + try { + return !( + node.compareDocumentPosition(node.ownerDocument.documentElement) & + node.DOCUMENT_POSITION_DISCONNECTED + ); + } catch (e) { + // "can't access dead object" error + return false; + } +} +exports.isNodeConnected = isNodeConnected; + +/** + * Determine whether a node is anonymous. + * + * @param {DOMNode} node + * @return {Boolean} + * + * FIXME(bug 1597411): Remove one of these (or both, as + * `node.isNativeAnonymous` is quite clear). + */ +const isAnonymous = node => node.isNativeAnonymous; +exports.isAnonymous = isAnonymous; +exports.isNativeAnonymous = isAnonymous; + +/** + * Determine whether a node is a template element. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isTemplateElement(node) { + return ( + node.ownerGlobal && node.ownerGlobal.HTMLTemplateElement.isInstance(node) + ); +} +exports.isTemplateElement = isTemplateElement; + +/** + * Determine whether a node is a shadow root. + * + * @param {DOMNode} node + * @return {Boolean} + */ +const isShadowRoot = node => node.containingShadowRoot == node; +exports.isShadowRoot = isShadowRoot; + +/* + * Gets the shadow root mode (open or closed). + * + * @param {DOMNode} node + * @return {String|null} + */ +function getShadowRootMode(node) { + return isShadowRoot(node) ? node.mode : null; +} +exports.getShadowRootMode = getShadowRootMode; + +/** + * Determine whether a node is a shadow host, ie. an element that has a shadowRoot + * attached to itself. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isShadowHost(node) { + const shadowRoot = node.openOrClosedShadowRoot; + return shadowRoot && shadowRoot.nodeType === Node.DOCUMENT_FRAGMENT_NODE; +} +exports.isShadowHost = isShadowHost; + +/** + * Determine whether a node is a child of a shadow host. Even if the element has been + * assigned to a slot in the attached shadow DOM, the parent node for this element is + * still considered to be the "host" element, and we need to walk them differently. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isDirectShadowHostChild(node) { + // Pseudo elements and native anonymous elements are always part of the anonymous tree. + if ( + isMarkerPseudoElement(node) || + isBeforePseudoElement(node) || + isAfterPseudoElement(node) || + node.isNativeAnonymous + ) { + return false; + } + + const parentNode = node.parentNode; + return parentNode && !!parentNode.openOrClosedShadowRoot; +} +exports.isDirectShadowHostChild = isDirectShadowHostChild; + +/** + * Determine whether a node is a ::marker pseudo. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isMarkerPseudoElement(node) { + return node.nodeName === "_moz_generated_content_marker"; +} +exports.isMarkerPseudoElement = isMarkerPseudoElement; + +/** + * Determine whether a node is a ::before pseudo. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isBeforePseudoElement(node) { + return node.nodeName === "_moz_generated_content_before"; +} +exports.isBeforePseudoElement = isBeforePseudoElement; + +/** + * Determine whether a node is a ::after pseudo. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isAfterPseudoElement(node) { + return node.nodeName === "_moz_generated_content_after"; +} +exports.isAfterPseudoElement = isAfterPseudoElement; + +/** + * Get the current zoom factor applied to the container window of a given node. + * @param {DOMNode|DOMWindow} + * The node for which the zoom factor should be calculated, or its + * owner window. + * @return {Number} + */ +function getCurrentZoom(node) { + const win = getWindowFor(node); + + if (!win) { + throw new Error("Unable to get the zoom from the given argument."); + } + + return win.browsingContext?.fullZoom || 1.0; +} +exports.getCurrentZoom = getCurrentZoom; + +/** + * Get the display pixel ratio for a given window. + * The `devicePixelRatio` property is affected by the zoom (see bug 809788), so we have to + * divide by the zoom value in order to get just the display density, expressed as pixel + * ratio (the physical display pixel compares to a pixel on a “normal” density screen). + * + * @param {DOMNode|DOMWindow} + * The node for which the zoom factor should be calculated, or its + * owner window. + * @return {Number} + */ +function getDisplayPixelRatio(node) { + const win = getWindowFor(node); + return win.devicePixelRatio / getCurrentZoom(node); +} +exports.getDisplayPixelRatio = getDisplayPixelRatio; + +/** + * Returns the window's dimensions for the `window` given. + * + * @return {Object} An object with `width` and `height` properties, representing the + * number of pixels for the document's size. + */ +function getWindowDimensions(window) { + // First we'll try without flushing layout, because it's way faster. + const windowUtils = utilsFor(window); + let { width, height } = windowUtils.getRootBounds(); + + if (!width || !height) { + // We need a flush after all :'( + width = window.innerWidth + window.scrollMaxX - window.scrollMinX; + height = window.innerHeight + window.scrollMaxY - window.scrollMinY; + + const scrollbarHeight = {}; + const scrollbarWidth = {}; + windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); + width -= scrollbarWidth.value; + height -= scrollbarHeight.value; + } + + return { width, height }; +} +exports.getWindowDimensions = getWindowDimensions; + +/** + * Returns the viewport's dimensions for the `window` given. + * + * @return {Object} An object with `width` and `height` properties, representing the + * number of pixels for the viewport's size. + */ +function getViewportDimensions(window) { + const windowUtils = utilsFor(window); + + const scrollbarHeight = {}; + const scrollbarWidth = {}; + windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); + + const width = window.innerWidth - scrollbarWidth.value; + const height = window.innerHeight - scrollbarHeight.value; + + return { width, height }; +} +exports.getViewportDimensions = getViewportDimensions; + +/** + * Return the default view for a given node, where node can be: + * - a DOM node + * - the document node + * - the window itself + * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for. + * @return {DOMWindow} + */ +function getWindowFor(node) { + if (Node.isInstance(node)) { + if (node.nodeType === node.DOCUMENT_NODE) { + return node.defaultView; + } + return node.ownerDocument.defaultView; + } else if (node instanceof Ci.nsIDOMWindow) { + return node; + } + return null; +} + +/** + * Synchronously loads a style sheet from `uri` and adds it to the list of + * additional style sheets of the document. + * The sheets added takes effect immediately, and only on the document of the + * `window` given. + * + * @param {DOMWindow} window + * @param {String} url + * @param {String} [type="agent"] + */ +function loadSheet(window, url, type = "agent") { + if (!(type in SHEET_TYPE)) { + type = "agent"; + } + + const windowUtils = utilsFor(window); + try { + windowUtils.loadSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]); + } catch (e) { + // The method fails if the url is already loaded. + } +} +exports.loadSheet = loadSheet; + +/** + * Remove the document style sheet at `sheetURI` from the list of additional + * style sheets of the document. The removal takes effect immediately. + * + * @param {DOMWindow} window + * @param {String} url + * @param {String} [type="agent"] + */ +function removeSheet(window, url, type = "agent") { + if (!(type in SHEET_TYPE)) { + type = "agent"; + } + + const windowUtils = utilsFor(window); + try { + windowUtils.removeSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]); + } catch (e) { + // The method fails if the url is already removed. + } +} +exports.removeSheet = removeSheet; + +/** + * Get the untransformed coordinates for a node. + * + * @param {DOMNode} node + * The node for which the DOMQuad is to be returned. + * @param {String} region + * The box model region to return: "content", "padding", "border" or + * "margin". + * @return {DOMQuad} + * A DOMQuad representation of the node. + */ +function getUntransformedQuad(node, region = "border") { + // Get the inverse transformation matrix for the node. + const matrix = node.getTransformToViewport(); + const inverse = matrix.inverse(); + const win = node.ownerGlobal; + + // Get the adjusted quads for the node (including scroll offsets). + const quads = getAdjustedQuads(win, node, region, { + ignoreZoom: true, + }); + + // Create DOMPoints from the transformed node position. + const p1 = new DOMPoint(quads[0].p1.x, quads[0].p1.y); + const p2 = new DOMPoint(quads[0].p2.x, quads[0].p2.y); + const p3 = new DOMPoint(quads[0].p3.x, quads[0].p3.y); + const p4 = new DOMPoint(quads[0].p4.x, quads[0].p4.y); + + // Apply the inverse transformation matrix to the points to get the + // untransformed points. + const ip1 = inverse.transformPoint(p1); + const ip2 = inverse.transformPoint(p2); + const ip3 = inverse.transformPoint(p3); + const ip4 = inverse.transformPoint(p4); + + // Save the results in a DOMQuad. + const quad = new DOMQuad( + { x: ip1.x, y: ip1.y }, + { x: ip2.x, y: ip2.y }, + { x: ip3.x, y: ip3.y }, + { x: ip4.x, y: ip4.y } + ); + + // Remove the border offsets because we include them when calculating + // offsets in the while loop. + const style = win.getComputedStyle(node); + const leftAdjustment = parseInt(style.borderLeftWidth, 10) || 0; + const topAdjustment = parseInt(style.borderTopWidth, 10) || 0; + + quad.p1.x -= leftAdjustment; + quad.p2.x -= leftAdjustment; + quad.p3.x -= leftAdjustment; + quad.p4.x -= leftAdjustment; + quad.p1.y -= topAdjustment; + quad.p2.y -= topAdjustment; + quad.p3.y -= topAdjustment; + quad.p4.y -= topAdjustment; + + // Calculate offsets. + while (node) { + const nodeStyle = win.getComputedStyle(node); + const borderLeftWidth = parseInt(nodeStyle.borderLeftWidth, 10) || 0; + const borderTopWidth = parseInt(nodeStyle.borderTopWidth, 10) || 0; + const leftOffset = node.offsetLeft - node.scrollLeft + borderLeftWidth; + const topOffset = node.offsetTop - node.scrollTop + borderTopWidth; + + quad.p1.x += leftOffset; + quad.p2.x += leftOffset; + quad.p3.x += leftOffset; + quad.p4.x += leftOffset; + quad.p1.y += topOffset; + quad.p2.y += topOffset; + quad.p3.y += topOffset; + quad.p4.y += topOffset; + + node = node.offsetParent; + } + + return quad; +} +exports.getUntransformedQuad = getUntransformedQuad; + +/** + * Calculate the total of the node and all of its ancestor's scrollTop and + * scrollLeft values. + * + * @param {DOMNode} node + * The node for which the absolute scroll offsets should be calculated. + * @return {Object} object + * An object containing scrollTop and scrollLeft values. + * @return {Number} object.scrollLeft + * The total scrollLeft values of the node and all of its ancestors. + * @return {Number} object.scrollTop + * The total scrollTop values of the node and all of its ancestors. + */ +function getAbsoluteScrollOffsetsForNode(node) { + const doc = node.ownerDocument; + + // Our walker will only iterate up to document.body so we start by saving the + // scroll values for `document.documentElement`. + let scrollTop = doc.documentElement.scrollTop; + let scrollLeft = doc.documentElement.scrollLeft; + const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT); + walker.currentNode = node; + let currentNode = walker.currentNode; + + // Iterate from `node` up the tree to `document.body` adding scroll offsets + // as we go. + while (currentNode) { + const nodeScrollTop = currentNode.scrollTop; + const nodeScrollLeft = currentNode.scrollLeft; + + if (nodeScrollTop || nodeScrollLeft) { + scrollTop += nodeScrollTop; + scrollLeft += nodeScrollLeft; + } + + currentNode = walker.parentNode(); + } + + return { + scrollLeft, + scrollTop, + }; +} +exports.getAbsoluteScrollOffsetsForNode = getAbsoluteScrollOffsetsForNode; + +/** + * Check if the provided node is a <frame> or <iframe> element. + * + * @param {DOMNode} node + * @returns {Boolean} + */ +function isFrame(node) { + const className = ChromeUtils.getClassName(node); + return className == "HTMLIFrameElement" || className == "HTMLFrameElement"; +} + +/** + * Check if the provided node is representing a remote <browser> element. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isRemoteBrowserElement(node) { + return ( + ChromeUtils.getClassName(node) == "XULFrameElement" && + !node.childNodes.length && + node.getAttribute("remote") == "true" + ); +} +exports.isRemoteBrowserElement = isRemoteBrowserElement; + +/** + * Check if the provided node is representing a remote frame. + * + * - In the context of the browser toolbox, a remote frame can be the <browser remote> + * element found inside each tab. + * - In the context of the content toolbox, a remote frame can be a <iframe> that contains + * a different origin document. + * + * @param {DOMNode} node + * @return {Boolean} + */ +function isRemoteFrame(node) { + if (isFrame(node)) { + return node.frameLoader?.isRemoteFrame; + } + + if (isRemoteBrowserElement(node)) { + return true; + } + + return false; +} +exports.isRemoteFrame = isRemoteFrame; + +/** + * Check if the provided node is representing a frame that has its own dedicated child target. + * + * @param {BrowsingContextTargetActor} targetActor + * @param {DOMNode} node + * @returns {Boolean} + */ +function isFrameWithChildTarget(targetActor, node) { + // If the iframe is blocked because of CSP, it won't have a document (and no associated targets) + if (isFrameBlockedByCSP(node)) { + return false; + } + + return isRemoteFrame(node) || (isFrame(node) && targetActor.ignoreSubFrames); +} + +exports.isFrameWithChildTarget = isFrameWithChildTarget; + +/** + * Check if the provided node is representing a frame that is blocked by CSP. + * + * @param {DOMNode} node + * @returns {Boolean} + */ +function isFrameBlockedByCSP(node) { + if (!isFrame(node)) { + return false; + } + + if (!node.src) { + return false; + } + + let uri; + try { + uri = lazy.NetUtil.newURI(node.src); + } catch (e) { + return false; + } + + const res = node.ownerDocument.csp.shouldLoad( + Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, + null, // nsICSPEventListener + uri, + null, // aOriginalURIIfRedirect + false, // aSendViolationReports + null, // aNonce + false // aParserCreated + ); + + return res !== Ci.nsIContentPolicy.ACCEPT; +} + +exports.isFrameBlockedByCSP = isFrameBlockedByCSP; |