1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. export interface Rect {
    
  11.   bottom: number;
    
  12.   height: number;
    
  13.   left: number;
    
  14.   right: number;
    
  15.   top: number;
    
  16.   width: number;
    
  17. }
    
  18. 
    
  19. // Get the window object for the document that a node belongs to,
    
  20. // or return null if it cannot be found (node not attached to DOM,
    
  21. // etc).
    
  22. export function getOwnerWindow(node: HTMLElement): typeof window | null {
    
  23.   if (!node.ownerDocument) {
    
  24.     return null;
    
  25.   }
    
  26.   return node.ownerDocument.defaultView;
    
  27. }
    
  28. 
    
  29. // Get the iframe containing a node, or return null if it cannot
    
  30. // be found (node not within iframe, etc).
    
  31. export function getOwnerIframe(node: HTMLElement): HTMLElement | null {
    
  32.   const nodeWindow = getOwnerWindow(node);
    
  33.   if (nodeWindow) {
    
  34.     return nodeWindow.frameElement;
    
  35.   }
    
  36.   return null;
    
  37. }
    
  38. 
    
  39. // Get a bounding client rect for a node, with an
    
  40. // offset added to compensate for its border.
    
  41. export function getBoundingClientRectWithBorderOffset(node: HTMLElement): Rect {
    
  42.   const dimensions = getElementDimensions(node);
    
  43.   return mergeRectOffsets([
    
  44.     node.getBoundingClientRect(),
    
  45.     {
    
  46.       top: dimensions.borderTop,
    
  47.       left: dimensions.borderLeft,
    
  48.       bottom: dimensions.borderBottom,
    
  49.       right: dimensions.borderRight,
    
  50.       // This width and height won't get used by mergeRectOffsets (since this
    
  51.       // is not the first rect in the array), but we set them so that this
    
  52.       // object type checks as a ClientRect.
    
  53.       width: 0,
    
  54.       height: 0,
    
  55.     },
    
  56.   ]);
    
  57. }
    
  58. 
    
  59. // Add together the top, left, bottom, and right properties of
    
  60. // each ClientRect, but keep the width and height of the first one.
    
  61. export function mergeRectOffsets(rects: Array<Rect>): Rect {
    
  62.   return rects.reduce((previousRect, rect) => {
    
  63.     if (previousRect == null) {
    
  64.       return rect;
    
  65.     }
    
  66. 
    
  67.     return {
    
  68.       top: previousRect.top + rect.top,
    
  69.       left: previousRect.left + rect.left,
    
  70.       width: previousRect.width,
    
  71.       height: previousRect.height,
    
  72.       bottom: previousRect.bottom + rect.bottom,
    
  73.       right: previousRect.right + rect.right,
    
  74.     };
    
  75.   });
    
  76. }
    
  77. 
    
  78. // Calculate a boundingClientRect for a node relative to boundaryWindow,
    
  79. // taking into account any offsets caused by intermediate iframes.
    
  80. export function getNestedBoundingClientRect(
    
  81.   node: HTMLElement,
    
  82.   boundaryWindow: typeof window,
    
  83. ): Rect {
    
  84.   const ownerIframe = getOwnerIframe(node);
    
  85.   if (ownerIframe && ownerIframe !== boundaryWindow) {
    
  86.     const rects: Array<Rect | ClientRect> = [node.getBoundingClientRect()];
    
  87.     let currentIframe: null | HTMLElement = ownerIframe;
    
  88.     let onlyOneMore = false;
    
  89.     while (currentIframe) {
    
  90.       const rect = getBoundingClientRectWithBorderOffset(currentIframe);
    
  91.       rects.push(rect);
    
  92.       currentIframe = getOwnerIframe(currentIframe);
    
  93. 
    
  94.       if (onlyOneMore) {
    
  95.         break;
    
  96.       }
    
  97.       // We don't want to calculate iframe offsets upwards beyond
    
  98.       // the iframe containing the boundaryWindow, but we
    
  99.       // need to calculate the offset relative to the boundaryWindow.
    
  100.       if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
    
  101.         onlyOneMore = true;
    
  102.       }
    
  103.     }
    
  104. 
    
  105.     return mergeRectOffsets(rects);
    
  106.   } else {
    
  107.     return node.getBoundingClientRect();
    
  108.   }
    
  109. }
    
  110. 
    
  111. export function getElementDimensions(domElement: Element): {
    
  112.   borderBottom: number,
    
  113.   borderLeft: number,
    
  114.   borderRight: number,
    
  115.   borderTop: number,
    
  116.   marginBottom: number,
    
  117.   marginLeft: number,
    
  118.   marginRight: number,
    
  119.   marginTop: number,
    
  120.   paddingBottom: number,
    
  121.   paddingLeft: number,
    
  122.   paddingRight: number,
    
  123.   paddingTop: number,
    
  124. } {
    
  125.   const calculatedStyle = window.getComputedStyle(domElement);
    
  126.   return {
    
  127.     borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
    
  128.     borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
    
  129.     borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
    
  130.     borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
    
  131.     marginLeft: parseInt(calculatedStyle.marginLeft, 10),
    
  132.     marginRight: parseInt(calculatedStyle.marginRight, 10),
    
  133.     marginTop: parseInt(calculatedStyle.marginTop, 10),
    
  134.     marginBottom: parseInt(calculatedStyle.marginBottom, 10),
    
  135.     paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
    
  136.     paddingRight: parseInt(calculatedStyle.paddingRight, 10),
    
  137.     paddingTop: parseInt(calculatedStyle.paddingTop, 10),
    
  138.     paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
    
  139.   };
    
  140. }