main
js 169 lines 5.13 KB
Raw
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 // $FlowFixMe[incompatible-variance]
44 return mergeRectOffsets([
45 node.getBoundingClientRect(),
46 {
47 top: dimensions.borderTop,
48 left: dimensions.borderLeft,
49 bottom: dimensions.borderBottom,
50 right: dimensions.borderRight,
51 // This width and height won't get used by mergeRectOffsets (since this
52 // is not the first rect in the array), but we set them so that this
53 // object type checks as a ClientRect.
54 width: 0,
55 height: 0,
56 },
57 ]);
58 }
59
60 // Add together the top, left, bottom, and right properties of
61 // each ClientRect, but keep the width and height of the first one.
62 export function mergeRectOffsets(rects: Array<Rect>): Rect {
63 return rects.reduce((previousRect, rect) => {
64 if (previousRect == null) {
65 return rect;
66 }
67
68 return {
69 top: previousRect.top + rect.top,
70 left: previousRect.left + rect.left,
71 width: previousRect.width,
72 height: previousRect.height,
73 bottom: previousRect.bottom + rect.bottom,
74 right: previousRect.right + rect.right,
75 };
76 });
77 }
78
79 // Calculate a boundingClientRect for a node relative to boundaryWindow,
80 // taking into account any offsets caused by intermediate iframes.
81 export function getNestedBoundingClientRect(
82 node: HTMLElement,
83 boundaryWindow: typeof window,
84 ): Rect {
85 const ownerIframe = getOwnerIframe(node);
86 if (ownerIframe && ownerIframe !== boundaryWindow) {
87 const rects: Array<Rect | ClientRect> = [node.getBoundingClientRect()];
88 let currentIframe: null | HTMLElement = ownerIframe;
89 let onlyOneMore = false;
90 while (currentIframe) {
91 const rect = getBoundingClientRectWithBorderOffset(currentIframe);
92 rects.push(rect);
93 currentIframe = getOwnerIframe(currentIframe);
94
95 if (onlyOneMore) {
96 break;
97 }
98 // We don't want to calculate iframe offsets upwards beyond
99 // the iframe containing the boundaryWindow, but we
100 // need to calculate the offset relative to the boundaryWindow.
101 if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
102 onlyOneMore = true;
103 }
104 }
105
106 // $FlowFixMe[incompatible-variance]
107 // $FlowFixMe[incompatible-type]
108 return mergeRectOffsets(rects);
109 } else {
110 // $FlowFixMe[incompatible-variance]
111 return node.getBoundingClientRect();
112 }
113 }
114
115 export function getElementDimensions(domElement: HTMLElement): {
116 borderBottom: number,
117 borderLeft: number,
118 borderRight: number,
119 borderTop: number,
120 marginBottom: number,
121 marginLeft: number,
122 marginRight: number,
123 marginTop: number,
124 paddingBottom: number,
125 paddingLeft: number,
126 paddingRight: number,
127 paddingTop: number,
128 } {
129 const calculatedStyle = window.getComputedStyle(domElement);
130 return {
131 borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
132 borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
133 borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
134 borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
135 marginLeft: parseInt(calculatedStyle.marginLeft, 10),
136 marginRight: parseInt(calculatedStyle.marginRight, 10),
137 marginTop: parseInt(calculatedStyle.marginTop, 10),
138 marginBottom: parseInt(calculatedStyle.marginBottom, 10),
139 paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
140 paddingRight: parseInt(calculatedStyle.paddingRight, 10),
141 paddingTop: parseInt(calculatedStyle.paddingTop, 10),
142 paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
143 };
144 }
145
146 export function extractHOCNames(displayName: string): {
147 baseComponentName: string,
148 hocNames: string[],
149 } {
150 if (!displayName) return {baseComponentName: '', hocNames: []};
151
152 const hocRegex = /^([A-Za-z_$][A-Za-z0-9_$]*)\((.*)\)$/;
153 const hocNames: string[] = [];
154 let baseComponentName = displayName;
155 let match;
156
157 while ((match = hocRegex.exec(baseComponentName)) != null) {
158 if (Array.isArray(match)) {
159 const [, hocName, inner] = match;
160 hocNames.push(hocName);
161 baseComponentName = inner;
162 }
163 }
164
165 return {
166 baseComponentName,
167 hocNames,
168 };
169 }