utils.js (24906B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 "DevToolsUtils", 10 "resource://devtools/shared/DevToolsUtils.js" 11 ); 12 const lazy = {}; 13 ChromeUtils.defineESModuleGetters( 14 lazy, 15 { 16 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 17 }, 18 { global: "contextual" } 19 ); 20 21 const SHEET_TYPE = { 22 agent: "AGENT_SHEET", 23 user: "USER_SHEET", 24 author: "AUTHOR_SHEET", 25 }; 26 27 // eslint-disable-next-line no-unused-vars 28 loader.lazyRequireGetter( 29 this, 30 "setIgnoreLayoutChanges", 31 "resource://devtools/server/actors/reflow.js", 32 true 33 ); 34 exports.setIgnoreLayoutChanges = (...args) => 35 this.setIgnoreLayoutChanges(...args); 36 37 /** 38 * Check a window is part of the boundary window given. 39 * 40 * @param {DOMWindow} boundaryWindow 41 * @param {DOMWindow} win 42 * @return {boolean} 43 */ 44 function isWindowIncluded(boundaryWindow, win) { 45 if (win === boundaryWindow) { 46 return true; 47 } 48 49 const parent = win.parent; 50 51 if (!parent || parent === win) { 52 return false; 53 } 54 55 return isWindowIncluded(boundaryWindow, parent); 56 } 57 exports.isWindowIncluded = isWindowIncluded; 58 59 /** 60 * like win.frameElement, but goes through mozbrowsers and mozapps iframes. 61 * 62 * @param {DOMWindow} win 63 * The window to get the frame for 64 * @return {DOMNode} 65 * The element in which the window is embedded. 66 */ 67 const getFrameElement = win => { 68 const isTopWindow = win && DevToolsUtils.getTopWindow(win) === win; 69 return isTopWindow ? null : win.browsingContext.embedderElement; 70 }; 71 exports.getFrameElement = getFrameElement; 72 73 /** 74 * Get the x/y offsets for of all the parent frames of a given node, limited to 75 * the boundary window given. 76 * 77 * @param {DOMWindow} boundaryWindow 78 * The window where to stop to iterate. If `null` is given, the top 79 * window is used. 80 * @param {DOMNode} node 81 * The node for which we are to get the offset 82 * @return {Array} 83 * The frame offset [x, y] 84 */ 85 function getFrameOffsets(boundaryWindow, node) { 86 let xOffset = 0; 87 let yOffset = 0; 88 89 let frameWin = getWindowFor(node); 90 const scale = getCurrentZoom(node); 91 92 if (boundaryWindow === null) { 93 boundaryWindow = DevToolsUtils.getTopWindow(frameWin); 94 } else if (typeof boundaryWindow === "undefined") { 95 throw new Error("No boundaryWindow given. Use null for the default one."); 96 } 97 98 while (frameWin !== boundaryWindow) { 99 const frameElement = getFrameElement(frameWin); 100 if (!frameElement) { 101 break; 102 } 103 104 // We are in an iframe. 105 // We take into account the parent iframe position and its 106 // offset (borders and padding). 107 const frameRect = frameElement.getBoundingClientRect(); 108 109 const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement); 110 111 xOffset += frameRect.left + offsetLeft; 112 yOffset += frameRect.top + offsetTop; 113 114 frameWin = frameWin.parent; 115 } 116 117 return [xOffset * scale, yOffset * scale]; 118 } 119 exports.getFrameOffsets = getFrameOffsets; 120 121 /** 122 * Get box quads adjusted for iframes and zoom level. 123 * 124 * Warning: this function returns things that look like DOMQuad objects but 125 * aren't (they resemble an old version of the spec). Unlike the return value 126 * of node.getBoxQuads, they have a .bounds property and not a .getBounds() 127 * method. 128 * 129 * @param {DOMWindow} boundaryWindow 130 * The window where to stop to iterate. If `null` is given, the top 131 * window is used. 132 * @param {DOMNode} node 133 * The node for which we are to get the box model region 134 * quads. 135 * @param {string} region 136 * The box model region to return: "content", "padding", "border" or 137 * "margin". 138 * @param {object} [options.ignoreZoom=false] 139 * Ignore zoom used in the context of e.g. canvas. 140 * @return {Array} 141 * An array of objects that have the same structure as quads returned by 142 * getBoxQuads. An empty array if the node has no quads or is invalid. 143 */ 144 function getAdjustedQuads( 145 boundaryWindow, 146 node, 147 region, 148 { ignoreZoom, ignoreScroll } = {} 149 ) { 150 if (!node || !node.getBoxQuads) { 151 return []; 152 } 153 154 const quads = node.getBoxQuads({ 155 box: region, 156 relativeTo: boundaryWindow.document, 157 createFramesForSuppressedWhitespace: false, 158 }); 159 160 if (!quads.length) { 161 return []; 162 } 163 164 const scale = ignoreZoom ? 1 : getCurrentZoom(node); 165 const { scrollX, scrollY } = ignoreScroll 166 ? { scrollX: 0, scrollY: 0 } 167 : boundaryWindow; 168 169 const xOffset = scrollX * scale; 170 const yOffset = scrollY * scale; 171 172 const adjustedQuads = []; 173 for (const quad of quads) { 174 const bounds = quad.getBounds(); 175 adjustedQuads.push({ 176 p1: { 177 w: quad.p1.w * scale, 178 x: quad.p1.x * scale + xOffset, 179 y: quad.p1.y * scale + yOffset, 180 z: quad.p1.z * scale, 181 }, 182 p2: { 183 w: quad.p2.w * scale, 184 x: quad.p2.x * scale + xOffset, 185 y: quad.p2.y * scale + yOffset, 186 z: quad.p2.z * scale, 187 }, 188 p3: { 189 w: quad.p3.w * scale, 190 x: quad.p3.x * scale + xOffset, 191 y: quad.p3.y * scale + yOffset, 192 z: quad.p3.z * scale, 193 }, 194 p4: { 195 w: quad.p4.w * scale, 196 x: quad.p4.x * scale + xOffset, 197 y: quad.p4.y * scale + yOffset, 198 z: quad.p4.z * scale, 199 }, 200 bounds: { 201 bottom: bounds.bottom * scale + yOffset, 202 height: bounds.height * scale, 203 left: bounds.left * scale + xOffset, 204 right: bounds.right * scale + xOffset, 205 top: bounds.top * scale + yOffset, 206 width: bounds.width * scale, 207 x: bounds.x * scale + xOffset, 208 y: bounds.y * scale + yOffset, 209 }, 210 }); 211 } 212 213 return adjustedQuads; 214 } 215 exports.getAdjustedQuads = getAdjustedQuads; 216 217 /** 218 * Compute the absolute position and the dimensions of a node, relativalely 219 * to the root window. 220 221 * @param {DOMWindow} boundaryWindow 222 * The window where to stop to iterate. If `null` is given, the top 223 * window is used. 224 * @param {DOMNode} node 225 * a DOM element to get the bounds for 226 * @param {DOMWindow} contentWindow 227 * the content window holding the node 228 * @return {object} 229 * A rect object with the {top, left, width, height} properties 230 */ 231 function getRect(boundaryWindow, node, contentWindow) { 232 let frameWin = node.ownerDocument.defaultView; 233 const clientRect = node.getBoundingClientRect(); 234 235 if (boundaryWindow === null) { 236 boundaryWindow = DevToolsUtils.getTopWindow(frameWin); 237 } else if (typeof boundaryWindow === "undefined") { 238 throw new Error("No boundaryWindow given. Use null for the default one."); 239 } 240 241 // Go up in the tree of frames to determine the correct rectangle. 242 // clientRect is read-only, we need to be able to change properties. 243 const rect = { 244 top: clientRect.top + contentWindow.pageYOffset, 245 left: clientRect.left + contentWindow.pageXOffset, 246 width: clientRect.width, 247 height: clientRect.height, 248 }; 249 250 // We iterate through all the parent windows. 251 while (frameWin !== boundaryWindow) { 252 const frameElement = getFrameElement(frameWin); 253 if (!frameElement) { 254 break; 255 } 256 257 // We are in an iframe. 258 // We take into account the parent iframe position and its 259 // offset (borders and padding). 260 const frameRect = frameElement.getBoundingClientRect(); 261 262 const [offsetTop, offsetLeft] = getFrameContentOffset(frameElement); 263 264 rect.top += frameRect.top + offsetTop; 265 rect.left += frameRect.left + offsetLeft; 266 267 frameWin = frameWin.parent; 268 } 269 270 return rect; 271 } 272 exports.getRect = getRect; 273 274 /** 275 * Get the 4 bounding points for a node taking iframes into account. 276 * Note that for transformed nodes, this will return the untransformed bound. 277 * 278 * @param {DOMWindow} boundaryWindow 279 * The window where to stop to iterate. If `null` is given, the top 280 * window is used. 281 * @param {DOMNode} node 282 * @return {object} 283 * An object with p1,p2,p3,p4 properties being {x,y} objects 284 */ 285 function getNodeBounds(boundaryWindow, node) { 286 if (!node) { 287 return null; 288 } 289 const { scrollX, scrollY } = boundaryWindow; 290 const scale = getCurrentZoom(node); 291 292 // Find out the offset of the node in its current frame 293 let offsetLeft = 0; 294 let offsetTop = 0; 295 let el = node; 296 while (el?.parentNode) { 297 offsetLeft += el.offsetLeft; 298 offsetTop += el.offsetTop; 299 el = el.offsetParent; 300 } 301 302 // Also take scrolled containers into account 303 el = node; 304 while (el?.parentNode) { 305 if (el.scrollTop) { 306 offsetTop -= el.scrollTop; 307 } 308 if (el.scrollLeft) { 309 offsetLeft -= el.scrollLeft; 310 } 311 el = el.parentNode; 312 } 313 314 // And add the potential frame offset if the node is nested 315 let [xOffset, yOffset] = getFrameOffsets(boundaryWindow, node); 316 xOffset += (offsetLeft + scrollX) * scale; 317 yOffset += (offsetTop + scrollY) * scale; 318 319 // Get the width and height 320 const width = node.offsetWidth * scale; 321 const height = node.offsetHeight * scale; 322 323 return { 324 p1: { x: xOffset, y: yOffset }, 325 p2: { x: xOffset + width, y: yOffset }, 326 p3: { x: xOffset + width, y: yOffset + height }, 327 p4: { x: xOffset, y: yOffset + height }, 328 top: yOffset, 329 right: xOffset + width, 330 bottom: yOffset + height, 331 left: xOffset, 332 width, 333 height, 334 }; 335 } 336 exports.getNodeBounds = getNodeBounds; 337 338 /** 339 * Same as doing iframe.contentWindow but works with all types of container 340 * elements that act like frames (e.g. <embed>), where 'contentWindow' isn't a 341 * property that can be accessed. 342 * This uses the inIDeepTreeWalker instead. 343 * 344 * @param {DOMNode} frame 345 * @return {Window} 346 */ 347 function safelyGetContentWindow(frame) { 348 if (frame.contentWindow) { 349 return frame.contentWindow; 350 } 351 352 const walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance( 353 Ci.inIDeepTreeWalker 354 ); 355 walker.showSubDocuments = true; 356 walker.showDocumentsAsNodes = true; 357 walker.init(frame); 358 walker.currentNode = frame; 359 360 const document = walker.nextNode(); 361 if (!document || !document.defaultView) { 362 throw new Error("Couldn't get the content window inside frame " + frame); 363 } 364 365 return document.defaultView; 366 } 367 368 /** 369 * Returns a frame's content offset (frame border + padding). 370 * Note: this function shouldn't need to exist, had the platform provided a 371 * suitable API for determining the offset between the frame's content and 372 * its bounding client rect. Bug 626359 should provide us with such an API. 373 * 374 * @param {DOMNode} frame 375 * The frame. 376 * @return {Array} [offsetTop, offsetLeft] 377 * offsetTop is the distance from the top of the frame and the top of 378 * the content document. 379 * offsetLeft is the distance from the left of the frame and the left 380 * of the content document. 381 */ 382 function getFrameContentOffset(frame) { 383 const style = safelyGetContentWindow(frame).getComputedStyle(frame); 384 385 // In some cases, the computed style is null 386 if (!style) { 387 return [0, 0]; 388 } 389 390 const paddingTop = parseInt(style.getPropertyValue("padding-top"), 10); 391 const paddingLeft = parseInt(style.getPropertyValue("padding-left"), 10); 392 393 const borderTop = parseInt(style.getPropertyValue("border-top-width"), 10); 394 const borderLeft = parseInt(style.getPropertyValue("border-left-width"), 10); 395 396 return [borderTop + paddingTop, borderLeft + paddingLeft]; 397 } 398 399 /** 400 * Check if a node and its document are still alive 401 * and attached to the window. 402 * 403 * @param {DOMNode} node 404 * @return {boolean} 405 */ 406 function isNodeConnected(node) { 407 if (!node.ownerDocument || !node.ownerDocument.defaultView) { 408 return false; 409 } 410 411 try { 412 return !( 413 node.compareDocumentPosition(node.ownerDocument.documentElement) & 414 node.DOCUMENT_POSITION_DISCONNECTED 415 ); 416 } catch (e) { 417 // "can't access dead object" error 418 return false; 419 } 420 } 421 exports.isNodeConnected = isNodeConnected; 422 423 /** 424 * Determine whether a node is a template element. 425 * 426 * @param {DOMNode} node 427 * @return {boolean} 428 */ 429 function isTemplateElement(node) { 430 return ( 431 node.ownerGlobal && node.ownerGlobal.HTMLTemplateElement.isInstance(node) 432 ); 433 } 434 exports.isTemplateElement = isTemplateElement; 435 436 /** 437 * Determine whether a node is a shadow root. 438 * 439 * @param {DOMNode} node 440 * @return {boolean} 441 */ 442 const isShadowRoot = node => node.containingShadowRoot == node; 443 exports.isShadowRoot = isShadowRoot; 444 445 /** 446 * Gets the shadow root mode (open or closed). 447 * 448 * @param {DOMNode} node 449 * @return {string | null} 450 */ 451 function getShadowRootMode(node) { 452 return isShadowRoot(node) ? node.mode : null; 453 } 454 exports.getShadowRootMode = getShadowRootMode; 455 456 /** 457 * Determine whether a node is a shadow host, ie. an element that has a shadowRoot 458 * attached to itself. 459 * 460 * @param {DOMNode} node 461 * @return {boolean} 462 */ 463 function isShadowHost(node) { 464 const shadowRoot = node.openOrClosedShadowRoot; 465 return shadowRoot && shadowRoot.nodeType === Node.DOCUMENT_FRAGMENT_NODE; 466 } 467 exports.isShadowHost = isShadowHost; 468 469 /** 470 * Determine whether a node is a child of a shadow host. Even if the element has been 471 * assigned to a slot in the attached shadow DOM, the parent node for this element is 472 * still considered to be the "host" element, and we need to walk them differently. 473 * 474 * @param {DOMNode} node 475 * @return {boolean} 476 */ 477 function isDirectShadowHostChild(node) { 478 // native anonymous elements (which includes pseudo elements) are always part of the 479 // anonymous tree. 480 if (node.isNativeAnonymous) { 481 return false; 482 } 483 484 const parentNode = node.parentNode; 485 return parentNode && !!parentNode.openOrClosedShadowRoot; 486 } 487 exports.isDirectShadowHostChild = isDirectShadowHostChild; 488 489 /** 490 * Get the current zoom factor applied to the container window of a given node. 491 * 492 * @param {DOMNode|DOMWindow} 493 * The node for which the zoom factor should be calculated, or its 494 * owner window. 495 * @return {number} 496 */ 497 function getCurrentZoom(node) { 498 const win = getWindowFor(node); 499 500 if (!win) { 501 throw new Error("Unable to get the zoom from the given argument."); 502 } 503 504 return win.browsingContext?.fullZoom || 1.0; 505 } 506 exports.getCurrentZoom = getCurrentZoom; 507 508 /** 509 * Get the display pixel ratio for a given window. 510 * The `devicePixelRatio` property is affected by the zoom (see bug 809788), so we have to 511 * divide by the zoom value in order to get just the display density, expressed as pixel 512 * ratio (the physical display pixel compares to a pixel on a “normal” density screen). 513 * 514 * @param {DOMNode|DOMWindow} 515 * The node for which the zoom factor should be calculated, or its 516 * owner window. 517 * @return {number} 518 */ 519 function getDisplayPixelRatio(node) { 520 const win = getWindowFor(node); 521 return win.devicePixelRatio / getCurrentZoom(node); 522 } 523 exports.getDisplayPixelRatio = getDisplayPixelRatio; 524 525 /** 526 * Returns the window's dimensions for the `window` given. 527 * 528 * @return {object} An object with `width` and `height` properties, representing the 529 * number of pixels for the document's size. 530 */ 531 function getWindowDimensions(window) { 532 // First we'll try without flushing layout, because it's way faster. 533 const { windowUtils } = window; 534 let { width, height } = windowUtils.getRootBounds(); 535 536 if (!width || !height) { 537 // We need a flush after all :'( 538 width = window.innerWidth + window.scrollMaxX - window.scrollMinX; 539 height = window.innerHeight + window.scrollMaxY - window.scrollMinY; 540 541 const scrollbarHeight = {}; 542 const scrollbarWidth = {}; 543 windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); 544 width -= scrollbarWidth.value; 545 height -= scrollbarHeight.value; 546 } 547 548 return { width, height }; 549 } 550 exports.getWindowDimensions = getWindowDimensions; 551 552 /** 553 * Returns the viewport's dimensions for the `window` given. 554 * 555 * @return {object} An object with `width` and `height` properties, representing the 556 * number of pixels for the viewport's size. 557 */ 558 function getViewportDimensions(window) { 559 const { windowUtils } = window; 560 561 const scrollbarHeight = {}; 562 const scrollbarWidth = {}; 563 windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); 564 565 const width = window.innerWidth - scrollbarWidth.value; 566 const height = window.innerHeight - scrollbarHeight.value; 567 568 return { width, height }; 569 } 570 exports.getViewportDimensions = getViewportDimensions; 571 572 /** 573 * Return the default view for a given node, where node can be: 574 * - a DOM node 575 * - the document node 576 * - the window itself 577 * 578 * @param {DOMNode|DOMWindow|DOMDocument} node The node to get the window for. 579 * @return {DOMWindow} 580 */ 581 function getWindowFor(node) { 582 if (Node.isInstance(node)) { 583 if (node.nodeType === node.DOCUMENT_NODE) { 584 return node.defaultView; 585 } 586 return node.ownerDocument.defaultView; 587 } else if (node instanceof Ci.nsIDOMWindow) { 588 return node; 589 } 590 return null; 591 } 592 593 /** 594 * Synchronously loads a style sheet from `uri` and adds it to the list of 595 * additional style sheets of the document. 596 * The sheets added takes effect immediately, and only on the document of the 597 * `window` given. 598 * 599 * @param {DOMWindow} window 600 * @param {string} url 601 * @param {string} [type="agent"] 602 */ 603 function loadSheet(window, url, type = "agent") { 604 if (!(type in SHEET_TYPE)) { 605 type = "agent"; 606 } 607 608 const { windowUtils } = window; 609 try { 610 windowUtils.loadSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]); 611 } catch (e) { 612 // The method fails if the url is already loaded. 613 } 614 } 615 exports.loadSheet = loadSheet; 616 617 /** 618 * Remove the document style sheet at `sheetURI` from the list of additional 619 * style sheets of the document. The removal takes effect immediately. 620 * 621 * @param {DOMWindow} window 622 * @param {string} url 623 * @param {string} [type="agent"] 624 */ 625 function removeSheet(window, url, type = "agent") { 626 if (!(type in SHEET_TYPE)) { 627 type = "agent"; 628 } 629 630 const { windowUtils } = window; 631 try { 632 windowUtils.removeSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]); 633 } catch (e) { 634 // The method fails if the url is already removed. 635 } 636 } 637 exports.removeSheet = removeSheet; 638 639 /** 640 * Get the untransformed coordinates for a node. 641 * 642 * @param {DOMNode} node 643 * The node for which the DOMQuad is to be returned. 644 * @param {string} region 645 * The box model region to return: "content", "padding", "border" or 646 * "margin". 647 * @return {DOMQuad} 648 * A DOMQuad representation of the node. 649 */ 650 function getUntransformedQuad(node, region = "border") { 651 // Get the inverse transformation matrix for the node. 652 const matrix = node.getTransformToViewport(); 653 const inverse = matrix.inverse(); 654 const win = node.ownerGlobal; 655 656 // Get the adjusted quads for the node (including scroll offsets). 657 const quads = getAdjustedQuads(win, node, region, { 658 ignoreZoom: true, 659 }); 660 661 // Create DOMPoints from the transformed node position. 662 const p1 = new DOMPoint(quads[0].p1.x, quads[0].p1.y); 663 const p2 = new DOMPoint(quads[0].p2.x, quads[0].p2.y); 664 const p3 = new DOMPoint(quads[0].p3.x, quads[0].p3.y); 665 const p4 = new DOMPoint(quads[0].p4.x, quads[0].p4.y); 666 667 // Apply the inverse transformation matrix to the points to get the 668 // untransformed points. 669 const ip1 = inverse.transformPoint(p1); 670 const ip2 = inverse.transformPoint(p2); 671 const ip3 = inverse.transformPoint(p3); 672 const ip4 = inverse.transformPoint(p4); 673 674 // Save the results in a DOMQuad. 675 const quad = new DOMQuad( 676 { x: ip1.x, y: ip1.y }, 677 { x: ip2.x, y: ip2.y }, 678 { x: ip3.x, y: ip3.y }, 679 { x: ip4.x, y: ip4.y } 680 ); 681 682 // Remove the border offsets because we include them when calculating 683 // offsets in the while loop. 684 const style = win.getComputedStyle(node); 685 const leftAdjustment = parseInt(style.borderLeftWidth, 10) || 0; 686 const topAdjustment = parseInt(style.borderTopWidth, 10) || 0; 687 688 quad.p1.x -= leftAdjustment; 689 quad.p2.x -= leftAdjustment; 690 quad.p3.x -= leftAdjustment; 691 quad.p4.x -= leftAdjustment; 692 quad.p1.y -= topAdjustment; 693 quad.p2.y -= topAdjustment; 694 quad.p3.y -= topAdjustment; 695 quad.p4.y -= topAdjustment; 696 697 // Calculate offsets. 698 while (node) { 699 const nodeStyle = win.getComputedStyle(node); 700 const borderLeftWidth = parseInt(nodeStyle.borderLeftWidth, 10) || 0; 701 const borderTopWidth = parseInt(nodeStyle.borderTopWidth, 10) || 0; 702 const leftOffset = node.offsetLeft - node.scrollLeft + borderLeftWidth; 703 const topOffset = node.offsetTop - node.scrollTop + borderTopWidth; 704 705 quad.p1.x += leftOffset; 706 quad.p2.x += leftOffset; 707 quad.p3.x += leftOffset; 708 quad.p4.x += leftOffset; 709 quad.p1.y += topOffset; 710 quad.p2.y += topOffset; 711 quad.p3.y += topOffset; 712 quad.p4.y += topOffset; 713 714 node = node.offsetParent; 715 } 716 717 return quad; 718 } 719 exports.getUntransformedQuad = getUntransformedQuad; 720 721 /** 722 * Calculate the total of the node and all of its ancestor's scrollTop and 723 * scrollLeft values. 724 * 725 * @param {DOMNode} node 726 * The node for which the absolute scroll offsets should be calculated. 727 * @return {object} object 728 * An object containing scrollTop and scrollLeft values. 729 * @return {number} object.scrollLeft 730 * The total scrollLeft values of the node and all of its ancestors. 731 * @return {number} object.scrollTop 732 * The total scrollTop values of the node and all of its ancestors. 733 */ 734 function getAbsoluteScrollOffsetsForNode(node) { 735 const doc = node.ownerDocument; 736 737 // Our walker will only iterate up to document.body so we start by saving the 738 // scroll values for `document.documentElement`. 739 let scrollTop = doc.documentElement.scrollTop; 740 let scrollLeft = doc.documentElement.scrollLeft; 741 const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT); 742 walker.currentNode = node; 743 let currentNode = walker.currentNode; 744 745 // Iterate from `node` up the tree to `document.body` adding scroll offsets 746 // as we go. 747 while (currentNode) { 748 const nodeScrollTop = currentNode.scrollTop; 749 const nodeScrollLeft = currentNode.scrollLeft; 750 751 if (nodeScrollTop || nodeScrollLeft) { 752 scrollTop += nodeScrollTop; 753 scrollLeft += nodeScrollLeft; 754 } 755 756 currentNode = walker.parentNode(); 757 } 758 759 return { 760 scrollLeft, 761 scrollTop, 762 }; 763 } 764 exports.getAbsoluteScrollOffsetsForNode = getAbsoluteScrollOffsetsForNode; 765 766 /** 767 * Check if the provided node is a <frame> or <iframe> element. 768 * 769 * @param {DOMNode} node 770 * @returns {boolean} 771 */ 772 function isFrame(node) { 773 const className = ChromeUtils.getClassName(node); 774 return className == "HTMLIFrameElement" || className == "HTMLFrameElement"; 775 } 776 777 /** 778 * Check if the provided node is representing a remote <browser> element. 779 * 780 * @param {DOMNode} node 781 * @return {boolean} 782 */ 783 function isRemoteBrowserElement(node) { 784 return ( 785 ChromeUtils.getClassName(node) == "XULFrameElement" && 786 !node.childNodes.length && 787 node.getAttribute("remote") == "true" 788 ); 789 } 790 exports.isRemoteBrowserElement = isRemoteBrowserElement; 791 792 /** 793 * Check if the provided node is representing a remote frame. 794 * 795 * - In the context of the browser toolbox, a remote frame can be the <browser remote> 796 * element found inside each tab. 797 * - In the context of the content toolbox, a remote frame can be a <iframe> that contains 798 * a different origin document. 799 * 800 * @param {DOMNode} node 801 * @return {boolean} 802 */ 803 function isRemoteFrame(node) { 804 if (isFrame(node)) { 805 return node.frameLoader?.isRemoteFrame; 806 } 807 808 if (isRemoteBrowserElement(node)) { 809 return true; 810 } 811 812 return false; 813 } 814 exports.isRemoteFrame = isRemoteFrame; 815 816 /** 817 * Check if the provided node is representing a frame that has its own dedicated child target. 818 * 819 * @param {BrowsingContextTargetActor} targetActor 820 * @param {DOMNode} node 821 * @returns {boolean} 822 */ 823 function isFrameWithChildTarget(targetActor, node) { 824 // If the iframe is blocked because of CSP, it won't have a document (and no associated targets) 825 if (isFrameBlockedByCSP(node)) { 826 return false; 827 } 828 829 return isRemoteFrame(node) || (isFrame(node) && targetActor.ignoreSubFrames); 830 } 831 832 exports.isFrameWithChildTarget = isFrameWithChildTarget; 833 834 /** 835 * Check if the provided node is representing a frame that is blocked by CSP. 836 * 837 * @param {DOMNode} node 838 * @returns {boolean} 839 */ 840 function isFrameBlockedByCSP(node) { 841 if (!isFrame(node)) { 842 return false; 843 } 844 845 if (!node.src) { 846 return false; 847 } 848 849 let uri; 850 try { 851 uri = lazy.NetUtil.newURI(node.src); 852 } catch (e) { 853 return false; 854 } 855 856 const res = node.ownerDocument.policyContainer.csp.shouldLoad( 857 Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, 858 null, // nsICSPEventListener 859 null, // nsILoadInfo 860 uri, 861 null, // aOriginalURIIfRedirect 862 false // aSendViolationReports 863 ); 864 865 return res !== Ci.nsIContentPolicy.ACCEPT; 866 } 867 868 exports.isFrameBlockedByCSP = isFrameBlockedByCSP;