@tamagui/use-element-layout 1.130.6 → 1.130.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +39 -82
- package/dist/cjs/index.js +42 -55
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index.native.js +53 -67
- package/dist/cjs/index.native.js.map +2 -2
- package/dist/esm/index.js +42 -55
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +32 -79
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +40 -81
- package/dist/esm/index.native.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +91 -81
- package/types/index.d.ts +19 -7
- package/types/index.d.ts.map +13 -11
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
2
2
|
import { isEqualShallow } from '@tamagui/is-equal-shallow'
|
|
3
|
-
import type
|
|
3
|
+
import { useCallback, type RefObject } from 'react'
|
|
4
4
|
|
|
5
5
|
const LayoutHandlers = new WeakMap<HTMLElement, Function>()
|
|
6
6
|
const Nodes = new Set<HTMLElement>()
|
|
@@ -26,8 +26,8 @@ export type LayoutValue = {
|
|
|
26
26
|
y: number
|
|
27
27
|
width: number
|
|
28
28
|
height: number
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
pageX: number
|
|
30
|
+
pageY: number
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export type LayoutEvent = {
|
|
@@ -58,7 +58,7 @@ export function enable(): void {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function
|
|
61
|
+
function startGlobalObservers() {
|
|
62
62
|
if (!isClient || globalIntersectionObserver) return
|
|
63
63
|
|
|
64
64
|
globalIntersectionObserver = new IntersectionObserver(
|
|
@@ -150,7 +150,9 @@ if (isClient) {
|
|
|
150
150
|
|
|
151
151
|
// only run once in a few frames, this could be adjustable
|
|
152
152
|
let frameCount = 0
|
|
153
|
-
|
|
153
|
+
|
|
154
|
+
const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP
|
|
155
|
+
const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 10
|
|
154
156
|
|
|
155
157
|
function layoutOnAnimationFrame() {
|
|
156
158
|
if (strategy !== 'off') {
|
|
@@ -193,76 +195,11 @@ export const getElementLayoutEvent = (
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
export const measureLayout = (
|
|
197
|
-
node: HTMLElement,
|
|
198
|
-
relativeTo: HTMLElement | null,
|
|
199
|
-
callback: (
|
|
200
|
-
x: number,
|
|
201
|
-
y: number,
|
|
202
|
-
width: number,
|
|
203
|
-
height: number,
|
|
204
|
-
left: number,
|
|
205
|
-
top: number
|
|
206
|
-
) => void
|
|
207
|
-
): void => {
|
|
208
|
-
const relativeNode = relativeTo || node?.parentElement
|
|
209
|
-
if (relativeNode instanceof HTMLElement) {
|
|
210
|
-
const nodeDim = node.getBoundingClientRect()
|
|
211
|
-
const relativeNodeDim = relativeNode.getBoundingClientRect()
|
|
212
|
-
|
|
213
|
-
if (relativeNodeDim && nodeDim) {
|
|
214
|
-
const { x, y, width, height, left, top } = getRelativeDimensions(
|
|
215
|
-
nodeDim,
|
|
216
|
-
relativeNodeDim
|
|
217
|
-
)
|
|
218
|
-
callback(x, y, width, height, left, top)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export const getElementLayoutEventAsync = async (
|
|
224
|
-
target: HTMLElement
|
|
225
|
-
): Promise<LayoutEvent> => {
|
|
226
|
-
const layout = await measureLayoutAsync(target)
|
|
227
|
-
if (!layout) {
|
|
228
|
-
throw new Error(`‼️`) // impossible
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
nativeEvent: {
|
|
232
|
-
layout,
|
|
233
|
-
target,
|
|
234
|
-
},
|
|
235
|
-
timeStamp: Date.now(),
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export const measureLayoutAsync = async (
|
|
240
|
-
node: HTMLElement,
|
|
241
|
-
relativeTo?: HTMLElement | null
|
|
242
|
-
): Promise<null | LayoutValue> => {
|
|
243
|
-
const relativeNode = relativeTo || node?.parentElement
|
|
244
|
-
if (relativeNode instanceof HTMLElement) {
|
|
245
|
-
const [nodeDim, relativeNodeDim] = await Promise.all([
|
|
246
|
-
getBoundingClientRectAsync(node),
|
|
247
|
-
getBoundingClientRectAsync(relativeNode),
|
|
248
|
-
])
|
|
249
|
-
|
|
250
|
-
if (relativeNodeDim && nodeDim) {
|
|
251
|
-
const { x, y, width, height, left, top } = getRelativeDimensions(
|
|
252
|
-
nodeDim,
|
|
253
|
-
relativeNodeDim
|
|
254
|
-
)
|
|
255
|
-
return { x, y, width, height, left, top }
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
260
|
-
|
|
261
198
|
const getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {
|
|
262
199
|
const { height, left, top, width } = a
|
|
263
200
|
const x = left - b.left
|
|
264
201
|
const y = top - b.top
|
|
265
|
-
return { x, y, width, height, left, top }
|
|
202
|
+
return { x, y, width, height, pageX: a.left, pageY: a.top }
|
|
266
203
|
}
|
|
267
204
|
|
|
268
205
|
export function useElementLayout(
|
|
@@ -283,7 +220,7 @@ export function useElementLayout(
|
|
|
283
220
|
Nodes.add(node)
|
|
284
221
|
|
|
285
222
|
// Add node to intersection observer
|
|
286
|
-
|
|
223
|
+
startGlobalObservers()
|
|
287
224
|
if (globalIntersectionObserver) {
|
|
288
225
|
globalIntersectionObserver.observe(node)
|
|
289
226
|
// Initialize as intersecting by default
|
|
@@ -323,7 +260,7 @@ function ensureWebElement<X>(x: X): HTMLElement | undefined {
|
|
|
323
260
|
return x instanceof HTMLElement ? x : undefined
|
|
324
261
|
}
|
|
325
262
|
|
|
326
|
-
const getBoundingClientRectAsync = (
|
|
263
|
+
export const getBoundingClientRectAsync = (
|
|
327
264
|
node: HTMLElement | null
|
|
328
265
|
): Promise<DOMRectReadOnly | false> => {
|
|
329
266
|
return new Promise<DOMRectReadOnly | false>((res) => {
|
|
@@ -342,14 +279,87 @@ const getBoundingClientRectAsync = (
|
|
|
342
279
|
})
|
|
343
280
|
}
|
|
344
281
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
282
|
+
export const measureNode = async (
|
|
283
|
+
node: HTMLElement,
|
|
284
|
+
relativeTo?: HTMLElement | null
|
|
285
|
+
): Promise<null | LayoutValue> => {
|
|
286
|
+
const relativeNode = relativeTo || node?.parentElement
|
|
287
|
+
if (relativeNode instanceof HTMLElement) {
|
|
288
|
+
const [nodeDim, relativeNodeDim] = await Promise.all([
|
|
289
|
+
getBoundingClientRectAsync(node),
|
|
290
|
+
getBoundingClientRectAsync(relativeNode),
|
|
291
|
+
])
|
|
292
|
+
if (relativeNodeDim && nodeDim) {
|
|
293
|
+
return getRelativeDimensions(nodeDim, relativeNodeDim)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void
|
|
300
|
+
|
|
301
|
+
type MeasureCb = (
|
|
302
|
+
x: number,
|
|
303
|
+
y: number,
|
|
304
|
+
width: number,
|
|
305
|
+
height: number,
|
|
306
|
+
pageX: number,
|
|
307
|
+
pageY: number
|
|
308
|
+
) => void
|
|
309
|
+
|
|
310
|
+
export const measure = async (
|
|
311
|
+
node: HTMLElement,
|
|
312
|
+
callback: MeasureCb
|
|
313
|
+
): Promise<LayoutValue | null> => {
|
|
314
|
+
const out = await measureNode(
|
|
315
|
+
node,
|
|
316
|
+
node.parentNode instanceof HTMLElement ? node.parentNode : null
|
|
317
|
+
)
|
|
318
|
+
if (out) {
|
|
319
|
+
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
320
|
+
}
|
|
321
|
+
return out
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function createMeasure(
|
|
325
|
+
node: HTMLElement
|
|
326
|
+
): (callback: MeasureCb) => Promise<LayoutValue | null> {
|
|
327
|
+
return (callback) => measure(node, callback)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
type WindowLayout = { pageX: number; pageY: number; width: number; height: number }
|
|
331
|
+
|
|
332
|
+
export const measureInWindow = async (
|
|
333
|
+
node: HTMLElement,
|
|
334
|
+
callback: MeasureInWindowCb
|
|
335
|
+
): Promise<WindowLayout | null> => {
|
|
336
|
+
const out = await measureNode(node, null)
|
|
337
|
+
if (out) {
|
|
338
|
+
callback?.(out.pageX, out.pageY, out.width, out.height)
|
|
339
|
+
}
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export const createMeasureInWindow = (
|
|
344
|
+
node: HTMLElement
|
|
345
|
+
): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) => {
|
|
346
|
+
return (callback) => measureInWindow(node, callback)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export const measureLayout = async (
|
|
350
|
+
node: HTMLElement,
|
|
351
|
+
relativeNode: HTMLElement,
|
|
352
|
+
callback: MeasureCb
|
|
353
|
+
): Promise<LayoutValue | null> => {
|
|
354
|
+
const out = await measureNode(node, relativeNode)
|
|
355
|
+
if (out) {
|
|
356
|
+
callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)
|
|
357
|
+
}
|
|
358
|
+
return out
|
|
348
359
|
}
|
|
349
360
|
|
|
350
|
-
export
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return { x, y, width: node.offsetWidth, height: node.offsetHeight, top, left }
|
|
361
|
+
export function createMeasureLayout(
|
|
362
|
+
node: HTMLElement
|
|
363
|
+
): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null> {
|
|
364
|
+
return (relativeTo, callback) => measureLayout(node, relativeTo, callback)
|
|
355
365
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
2
|
type TamaguiComponentStatePartial = {
|
|
3
3
|
host?: any;
|
|
4
4
|
};
|
|
@@ -9,8 +9,8 @@ export type LayoutValue = {
|
|
|
9
9
|
y: number;
|
|
10
10
|
width: number;
|
|
11
11
|
height: number;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
pageX: number;
|
|
13
|
+
pageY: number;
|
|
14
14
|
};
|
|
15
15
|
export type LayoutEvent = {
|
|
16
16
|
nativeEvent: {
|
|
@@ -21,11 +21,23 @@ export type LayoutEvent = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare function enable(): void;
|
|
23
23
|
export declare const getElementLayoutEvent: (nodeRect: DOMRectReadOnly, parentRect: DOMRectReadOnly) => LayoutEvent;
|
|
24
|
-
export declare const measureLayout: (node: HTMLElement, relativeTo: HTMLElement | null, callback: (x: number, y: number, width: number, height: number, left: number, top: number) => void) => void;
|
|
25
|
-
export declare const getElementLayoutEventAsync: (target: HTMLElement) => Promise<LayoutEvent>;
|
|
26
|
-
export declare const measureLayoutAsync: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
|
|
27
24
|
export declare function useElementLayout(ref: RefObject<TamaguiComponentStatePartial>, onLayout?: ((e: LayoutEvent) => void) | null): void;
|
|
28
|
-
export declare const
|
|
25
|
+
export declare const getBoundingClientRectAsync: (node: HTMLElement | null) => Promise<DOMRectReadOnly | false>;
|
|
26
|
+
export declare const measureNode: (node: HTMLElement, relativeTo?: HTMLElement | null) => Promise<null | LayoutValue>;
|
|
27
|
+
type MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void;
|
|
28
|
+
type MeasureCb = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
29
|
+
export declare const measure: (node: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
30
|
+
export declare function createMeasure(node: HTMLElement): (callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
31
|
+
type WindowLayout = {
|
|
32
|
+
pageX: number;
|
|
33
|
+
pageY: number;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
export declare const measureInWindow: (node: HTMLElement, callback: MeasureInWindowCb) => Promise<WindowLayout | null>;
|
|
38
|
+
export declare const createMeasureInWindow: (node: HTMLElement) => ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>);
|
|
39
|
+
export declare const measureLayout: (node: HTMLElement, relativeNode: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
40
|
+
export declare function createMeasureLayout(node: HTMLElement): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null>;
|
|
29
41
|
export {};
|
|
30
42
|
|
|
31
43
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
"mappings": "AAEA,
|
|
2
|
+
"mappings": "AAEA,cAA2B,iBAAiB,OAAO;KAS9C,+BAA+B;CAClC;AACD;KAEI,4BAA4B,QAAQ,SAAS;AAIlD,OAAO,iBAAS,oBAAoBA,OAAO;AAI3C,YAAY,cAAc;CACxB;CACA;CACA;CACA;CACA;CACA;AACD;AAED,YAAY,cAAc;CACxB,aAAa;EACX,QAAQ;EACR;CACD;CACD;AACD;AAYD,OAAO,iBAAS;AAsIhB,OAAO,cAAM,wBACXC,UAAU,iBACVC,YAAY,oBACX;AAiBH,OAAO,iBAAS,iBACdC,KAAK,UAAU,+BACfC,aAAaC,GAAG;AAwDlB,OAAO,cAAM,6BACXC,MAAM,uBACL,QAAQ,kBAAkB;AAiB7B,OAAO,cAAM,cACXC,MAAM,aACNC,aAAa,uBACZ,eAAe;KAcb,qBAAqBC,WAAWC,WAAWC,eAAeC;KAE1D,aACHH,WACAC,WACAC,eACAC,gBACAC,eACAC;AAGF,OAAO,cAAM,UACXP,MAAM,aACNQ,UAAU,cACT,QAAQ;AAWX,OAAO,iBAAS,cACdR,MAAM,eACJQ,UAAU,cAAc,QAAQ;KAI/B,eAAe;CAAE;CAAe;CAAe;CAAe;AAAgB;AAEnF,OAAO,cAAM,kBACXR,MAAM,aACNS,UAAU,sBACT,QAAQ;AAQX,OAAO,cAAM,wBACXT,MAAM,kBACHS,UAAU,sBAAsB,QAAQ;AAI7C,OAAO,cAAM,gBACXT,MAAM,aACNU,cAAc,aACdF,UAAU,cACT,QAAQ;AAQX,OAAO,iBAAS,oBACdR,MAAM,eACJW,YAAY,aAAaH,UAAU,cAAc,QAAQ",
|
|
3
3
|
"names": [
|
|
4
4
|
"state: LayoutMeasurementStrategy",
|
|
5
5
|
"nodeRect: DOMRectReadOnly",
|
|
6
6
|
"parentRect: DOMRectReadOnly",
|
|
7
|
+
"ref: RefObject<TamaguiComponentStatePartial>",
|
|
8
|
+
"onLayout?: ((e: LayoutEvent) => void) | null",
|
|
9
|
+
"e: LayoutEvent",
|
|
10
|
+
"node: HTMLElement | null",
|
|
7
11
|
"node: HTMLElement",
|
|
8
|
-
"relativeTo
|
|
9
|
-
"callback: (\n x: number,\n y: number,\n width: number,\n height: number,\n left: number,\n top: number\n ) => void",
|
|
12
|
+
"relativeTo?: HTMLElement | null",
|
|
10
13
|
"x: number",
|
|
11
14
|
"y: number",
|
|
12
15
|
"width: number",
|
|
13
16
|
"height: number",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"e: LayoutEvent"
|
|
17
|
+
"pageX: number",
|
|
18
|
+
"pageY: number",
|
|
19
|
+
"callback: MeasureCb",
|
|
20
|
+
"callback: MeasureInWindowCb",
|
|
21
|
+
"relativeNode: HTMLElement",
|
|
22
|
+
"relativeTo: HTMLElement"
|
|
21
23
|
],
|
|
22
24
|
"sources": [
|
|
23
25
|
"src/index.ts"
|
|
24
26
|
],
|
|
25
27
|
"sourcesContent": [
|
|
26
|
-
"import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { isEqualShallow } from '@tamagui/is-equal-shallow'\nimport type { RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\n\n// Single persistent IntersectionObserver for all nodes\nlet globalIntersectionObserver: IntersectionObserver | null = null\n\ntype TamaguiComponentStatePartial = {\n host?: any\n}\n\ntype LayoutMeasurementStrategy = 'off' | 'sync' | 'async'\n\nlet strategy: LayoutMeasurementStrategy = 'async'\n\nexport function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {\n strategy = state\n}\n\nexport type LayoutValue = {\n x: number\n y: number\n width: number\n height: number\n left: number\n top: number\n}\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: LayoutValue\n target: any\n }\n timeStamp: number\n}\n\nconst NodeRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst ParentRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\nconst rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined\n\n// prevent thrashing during first hydration (somewhat, streaming gets trickier)\nlet avoidUpdates = true\nconst queuedUpdates = new Map<HTMLElement, Function>()\n\nexport function enable(): void {\n if (avoidUpdates) {\n avoidUpdates = false\n if (queuedUpdates) {\n queuedUpdates.forEach((cb) => cb())\n queuedUpdates.clear()\n }\n }\n}\n\nfunction startGlobalIntersectionObserver() {\n if (!isClient || globalIntersectionObserver) return\n\n globalIntersectionObserver = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n const node = entry.target as HTMLElement\n if (IntersectionState.get(node) !== entry.isIntersecting) {\n IntersectionState.set(node, entry.isIntersecting)\n }\n })\n },\n {\n threshold: 0,\n }\n )\n}\n\nif (isClient) {\n if (rAF) {\n const supportsCheckVisibility = 'checkVisibility' in document.body\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n if (IntersectionState.get(node) === false) {\n // avoid due to not intersecting\n return\n }\n // triggers style recalculation in safari which is slower than not\n if (process.env.TAMAGUI_ONLAYOUT_VISIBILITY_CHECK === '1') {\n if (supportsCheckVisibility && !(node as any).checkVisibility()) {\n // avoid due to not visible\n return\n }\n }\n\n const onLayout = LayoutHandlers.get(node)\n if (typeof onLayout !== 'function') return\n\n const parentNode = node.parentElement\n if (!parentNode) return\n\n let nodeRect: DOMRectReadOnly\n let parentRect: DOMRectReadOnly\n\n if (strategy === 'async') {\n const [nr, pr] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(parentNode),\n ])\n\n if (nr === false || pr === false) {\n return\n }\n\n nodeRect = nr\n parentRect = pr\n } else {\n nodeRect = node.getBoundingClientRect()\n parentRect = parentNode.getBoundingClientRect()\n }\n\n const cachedRect = NodeRectCache.get(node)\n const cachedParentRect = NodeRectCache.get(parentNode)\n\n if (\n !cachedRect ||\n // has changed one rect\n // @ts-expect-error DOMRectReadOnly can go into object\n (!isEqualShallow(cachedRect, nodeRect) &&\n // @ts-expect-error DOMRectReadOnly can go into object\n (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))\n ) {\n NodeRectCache.set(node, nodeRect)\n ParentRectCache.set(parentNode, parentRect)\n\n const event = getElementLayoutEvent(nodeRect, parentRect)\n\n if (avoidUpdates) {\n queuedUpdates.set(node, () => onLayout(event))\n } else {\n onLayout(event)\n }\n }\n }\n\n // note that getBoundingClientRect() does not thrash layout if its after an animation frame\n // ok new note: *if* it needed recalc then yea, but browsers often skip that, so it does\n // which is why we use async strategy in general\n rAF!(layoutOnAnimationFrame)\n\n // only run once in a few frames, this could be adjustable\n let frameCount = 0\n const RUN_EVERY_X_FRAMES = 8\n\n function layoutOnAnimationFrame() {\n if (strategy !== 'off') {\n if (frameCount++ % RUN_EVERY_X_FRAMES !== 0) {\n // skip a few frames to avoid work\n rAF!(layoutOnAnimationFrame)\n return\n }\n\n if (frameCount === Number.MAX_SAFE_INTEGER) {\n frameCount = 0\n }\n\n Nodes.forEach((node) => {\n updateLayoutIfChanged(node)\n })\n }\n\n rAF!(layoutOnAnimationFrame)\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `No requestAnimationFrame - please polyfill for onLayout to work correctly`\n )\n }\n }\n}\n\nexport const getElementLayoutEvent = (\n nodeRect: DOMRectReadOnly,\n parentRect: DOMRectReadOnly\n): LayoutEvent => {\n return {\n nativeEvent: {\n layout: getRelativeDimensions(nodeRect, parentRect),\n target: nodeRect,\n },\n timeStamp: Date.now(),\n }\n}\n\nexport const measureLayout = (\n node: HTMLElement,\n relativeTo: HTMLElement | null,\n callback: (\n x: number,\n y: number,\n width: number,\n height: number,\n left: number,\n top: number\n ) => void\n): void => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const nodeDim = node.getBoundingClientRect()\n const relativeNodeDim = relativeNode.getBoundingClientRect()\n\n if (relativeNodeDim && nodeDim) {\n const { x, y, width, height, left, top } = getRelativeDimensions(\n nodeDim,\n relativeNodeDim\n )\n callback(x, y, width, height, left, top)\n }\n }\n}\n\nexport const getElementLayoutEventAsync = async (\n target: HTMLElement\n): Promise<LayoutEvent> => {\n const layout = await measureLayoutAsync(target)\n if (!layout) {\n throw new Error(`‼️`) // impossible\n }\n return {\n nativeEvent: {\n layout,\n target,\n },\n timeStamp: Date.now(),\n }\n}\n\nexport const measureLayoutAsync = async (\n node: HTMLElement,\n relativeTo?: HTMLElement | null\n): Promise<null | LayoutValue> => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const [nodeDim, relativeNodeDim] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(relativeNode),\n ])\n\n if (relativeNodeDim && nodeDim) {\n const { x, y, width, height, left, top } = getRelativeDimensions(\n nodeDim,\n relativeNodeDim\n )\n return { x, y, width, height, left, top }\n }\n }\n return null\n}\n\nconst getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {\n const { height, left, top, width } = a\n const x = left - b.left\n const y = top - b.top\n return { x, y, width, height, left, top }\n}\n\nexport function useElementLayout(\n ref: RefObject<TamaguiComponentStatePartial>,\n onLayout?: ((e: LayoutEvent) => void) | null\n): void {\n // ensure always up to date so we can avoid re-running effect\n const node = ensureWebElement(ref.current?.host)\n if (node && onLayout) {\n LayoutHandlers.set(node, onLayout)\n }\n\n useIsomorphicLayoutEffect(() => {\n if (!onLayout) return\n const node = ref.current?.host\n if (!node) return\n\n Nodes.add(node)\n\n // Add node to intersection observer\n startGlobalIntersectionObserver()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(node)\n // Initialize as intersecting by default\n IntersectionState.set(node, true)\n }\n\n // always do one immediate sync layout event no matter the strategy for accuracy\n const parentNode = node.parentNode\n if (parentNode) {\n onLayout(\n getElementLayoutEvent(\n node.getBoundingClientRect(),\n parentNode.getBoundingClientRect()\n )\n )\n }\n\n return () => {\n Nodes.delete(node)\n LayoutHandlers.delete(node)\n NodeRectCache.delete(node)\n LastChangeTime.delete(node)\n IntersectionState.delete(node)\n\n // Remove from intersection observer\n if (globalIntersectionObserver) {\n globalIntersectionObserver.unobserve(node)\n }\n }\n }, [ref, !!onLayout])\n}\n\nfunction ensureWebElement<X>(x: X): HTMLElement | undefined {\n if (typeof HTMLElement === 'undefined') {\n return undefined\n }\n return x instanceof HTMLElement ? x : undefined\n}\n\nconst getBoundingClientRectAsync = (\n node: HTMLElement | null\n): Promise<DOMRectReadOnly | false> => {\n return new Promise<DOMRectReadOnly | false>((res) => {\n if (!node || node.nodeType !== 1) return res(false)\n\n const io = new IntersectionObserver(\n (entries) => {\n io.disconnect()\n return res(entries[0].boundingClientRect)\n },\n {\n threshold: 0,\n }\n )\n io.observe(node)\n })\n}\n\nconst getBoundingClientRect = (node: HTMLElement | null): undefined | DOMRect => {\n if (!node || node.nodeType !== 1) return\n return node.getBoundingClientRect?.()\n}\n\nexport const getRect = (node: HTMLElement): LayoutValue | undefined => {\n const rect = getBoundingClientRect(node)\n if (!rect) return\n const { x, y, top, left } = rect\n return { x, y, width: node.offsetWidth, height: node.offsetHeight, top, left }\n}\n"
|
|
28
|
+
"import { isClient, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { isEqualShallow } from '@tamagui/is-equal-shallow'\nimport { useCallback, type RefObject } from 'react'\n\nconst LayoutHandlers = new WeakMap<HTMLElement, Function>()\nconst Nodes = new Set<HTMLElement>()\nconst IntersectionState = new WeakMap<HTMLElement, boolean>()\n\n// Single persistent IntersectionObserver for all nodes\nlet globalIntersectionObserver: IntersectionObserver | null = null\n\ntype TamaguiComponentStatePartial = {\n host?: any\n}\n\ntype LayoutMeasurementStrategy = 'off' | 'sync' | 'async'\n\nlet strategy: LayoutMeasurementStrategy = 'async'\n\nexport function setOnLayoutStrategy(state: LayoutMeasurementStrategy): void {\n strategy = state\n}\n\nexport type LayoutValue = {\n x: number\n y: number\n width: number\n height: number\n pageX: number\n pageY: number\n}\n\nexport type LayoutEvent = {\n nativeEvent: {\n layout: LayoutValue\n target: any\n }\n timeStamp: number\n}\n\nconst NodeRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst ParentRectCache = new WeakMap<HTMLElement, DOMRect>()\nconst LastChangeTime = new WeakMap<HTMLElement, number>()\n\nconst rAF = typeof window !== 'undefined' ? window.requestAnimationFrame : undefined\n\n// prevent thrashing during first hydration (somewhat, streaming gets trickier)\nlet avoidUpdates = true\nconst queuedUpdates = new Map<HTMLElement, Function>()\n\nexport function enable(): void {\n if (avoidUpdates) {\n avoidUpdates = false\n if (queuedUpdates) {\n queuedUpdates.forEach((cb) => cb())\n queuedUpdates.clear()\n }\n }\n}\n\nfunction startGlobalObservers() {\n if (!isClient || globalIntersectionObserver) return\n\n globalIntersectionObserver = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n const node = entry.target as HTMLElement\n if (IntersectionState.get(node) !== entry.isIntersecting) {\n IntersectionState.set(node, entry.isIntersecting)\n }\n })\n },\n {\n threshold: 0,\n }\n )\n}\n\nif (isClient) {\n if (rAF) {\n const supportsCheckVisibility = 'checkVisibility' in document.body\n\n async function updateLayoutIfChanged(node: HTMLElement) {\n if (IntersectionState.get(node) === false) {\n // avoid due to not intersecting\n return\n }\n // triggers style recalculation in safari which is slower than not\n if (process.env.TAMAGUI_ONLAYOUT_VISIBILITY_CHECK === '1') {\n if (supportsCheckVisibility && !(node as any).checkVisibility()) {\n // avoid due to not visible\n return\n }\n }\n\n const onLayout = LayoutHandlers.get(node)\n if (typeof onLayout !== 'function') return\n\n const parentNode = node.parentElement\n if (!parentNode) return\n\n let nodeRect: DOMRectReadOnly\n let parentRect: DOMRectReadOnly\n\n if (strategy === 'async') {\n const [nr, pr] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(parentNode),\n ])\n\n if (nr === false || pr === false) {\n return\n }\n\n nodeRect = nr\n parentRect = pr\n } else {\n nodeRect = node.getBoundingClientRect()\n parentRect = parentNode.getBoundingClientRect()\n }\n\n const cachedRect = NodeRectCache.get(node)\n const cachedParentRect = NodeRectCache.get(parentNode)\n\n if (\n !cachedRect ||\n // has changed one rect\n // @ts-expect-error DOMRectReadOnly can go into object\n (!isEqualShallow(cachedRect, nodeRect) &&\n // @ts-expect-error DOMRectReadOnly can go into object\n (!cachedParentRect || !isEqualShallow(cachedParentRect, parentRect)))\n ) {\n NodeRectCache.set(node, nodeRect)\n ParentRectCache.set(parentNode, parentRect)\n\n const event = getElementLayoutEvent(nodeRect, parentRect)\n\n if (avoidUpdates) {\n queuedUpdates.set(node, () => onLayout(event))\n } else {\n onLayout(event)\n }\n }\n }\n\n // note that getBoundingClientRect() does not thrash layout if its after an animation frame\n // ok new note: *if* it needed recalc then yea, but browsers often skip that, so it does\n // which is why we use async strategy in general\n rAF!(layoutOnAnimationFrame)\n\n // only run once in a few frames, this could be adjustable\n let frameCount = 0\n\n const userSkipVal = process.env.TAMAGUI_LAYOUT_FRAME_SKIP\n const RUN_EVERY_X_FRAMES = userSkipVal ? +userSkipVal : 10\n\n function layoutOnAnimationFrame() {\n if (strategy !== 'off') {\n if (frameCount++ % RUN_EVERY_X_FRAMES !== 0) {\n // skip a few frames to avoid work\n rAF!(layoutOnAnimationFrame)\n return\n }\n\n if (frameCount === Number.MAX_SAFE_INTEGER) {\n frameCount = 0\n }\n\n Nodes.forEach((node) => {\n updateLayoutIfChanged(node)\n })\n }\n\n rAF!(layoutOnAnimationFrame)\n }\n } else {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `No requestAnimationFrame - please polyfill for onLayout to work correctly`\n )\n }\n }\n}\n\nexport const getElementLayoutEvent = (\n nodeRect: DOMRectReadOnly,\n parentRect: DOMRectReadOnly\n): LayoutEvent => {\n return {\n nativeEvent: {\n layout: getRelativeDimensions(nodeRect, parentRect),\n target: nodeRect,\n },\n timeStamp: Date.now(),\n }\n}\n\nconst getRelativeDimensions = (a: DOMRectReadOnly, b: DOMRectReadOnly) => {\n const { height, left, top, width } = a\n const x = left - b.left\n const y = top - b.top\n return { x, y, width, height, pageX: a.left, pageY: a.top }\n}\n\nexport function useElementLayout(\n ref: RefObject<TamaguiComponentStatePartial>,\n onLayout?: ((e: LayoutEvent) => void) | null\n): void {\n // ensure always up to date so we can avoid re-running effect\n const node = ensureWebElement(ref.current?.host)\n if (node && onLayout) {\n LayoutHandlers.set(node, onLayout)\n }\n\n useIsomorphicLayoutEffect(() => {\n if (!onLayout) return\n const node = ref.current?.host\n if (!node) return\n\n Nodes.add(node)\n\n // Add node to intersection observer\n startGlobalObservers()\n if (globalIntersectionObserver) {\n globalIntersectionObserver.observe(node)\n // Initialize as intersecting by default\n IntersectionState.set(node, true)\n }\n\n // always do one immediate sync layout event no matter the strategy for accuracy\n const parentNode = node.parentNode\n if (parentNode) {\n onLayout(\n getElementLayoutEvent(\n node.getBoundingClientRect(),\n parentNode.getBoundingClientRect()\n )\n )\n }\n\n return () => {\n Nodes.delete(node)\n LayoutHandlers.delete(node)\n NodeRectCache.delete(node)\n LastChangeTime.delete(node)\n IntersectionState.delete(node)\n\n // Remove from intersection observer\n if (globalIntersectionObserver) {\n globalIntersectionObserver.unobserve(node)\n }\n }\n }, [ref, !!onLayout])\n}\n\nfunction ensureWebElement<X>(x: X): HTMLElement | undefined {\n if (typeof HTMLElement === 'undefined') {\n return undefined\n }\n return x instanceof HTMLElement ? x : undefined\n}\n\nexport const getBoundingClientRectAsync = (\n node: HTMLElement | null\n): Promise<DOMRectReadOnly | false> => {\n return new Promise<DOMRectReadOnly | false>((res) => {\n if (!node || node.nodeType !== 1) return res(false)\n\n const io = new IntersectionObserver(\n (entries) => {\n io.disconnect()\n return res(entries[0].boundingClientRect)\n },\n {\n threshold: 0,\n }\n )\n io.observe(node)\n })\n}\n\nexport const measureNode = async (\n node: HTMLElement,\n relativeTo?: HTMLElement | null\n): Promise<null | LayoutValue> => {\n const relativeNode = relativeTo || node?.parentElement\n if (relativeNode instanceof HTMLElement) {\n const [nodeDim, relativeNodeDim] = await Promise.all([\n getBoundingClientRectAsync(node),\n getBoundingClientRectAsync(relativeNode),\n ])\n if (relativeNodeDim && nodeDim) {\n return getRelativeDimensions(nodeDim, relativeNodeDim)\n }\n }\n return null\n}\n\ntype MeasureInWindowCb = (x: number, y: number, width: number, height: number) => void\n\ntype MeasureCb = (\n x: number,\n y: number,\n width: number,\n height: number,\n pageX: number,\n pageY: number\n) => void\n\nexport const measure = async (\n node: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(\n node,\n node.parentNode instanceof HTMLElement ? node.parentNode : null\n )\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasure(\n node: HTMLElement\n): (callback: MeasureCb) => Promise<LayoutValue | null> {\n return (callback) => measure(node, callback)\n}\n\ntype WindowLayout = { pageX: number; pageY: number; width: number; height: number }\n\nexport const measureInWindow = async (\n node: HTMLElement,\n callback: MeasureInWindowCb\n): Promise<WindowLayout | null> => {\n const out = await measureNode(node, null)\n if (out) {\n callback?.(out.pageX, out.pageY, out.width, out.height)\n }\n return out\n}\n\nexport const createMeasureInWindow = (\n node: HTMLElement\n): ((callback: MeasureInWindowCb) => Promise<WindowLayout | null>) => {\n return (callback) => measureInWindow(node, callback)\n}\n\nexport const measureLayout = async (\n node: HTMLElement,\n relativeNode: HTMLElement,\n callback: MeasureCb\n): Promise<LayoutValue | null> => {\n const out = await measureNode(node, relativeNode)\n if (out) {\n callback?.(out.x, out.y, out.width, out.height, out.pageX, out.pageY)\n }\n return out\n}\n\nexport function createMeasureLayout(\n node: HTMLElement\n): (relativeTo: HTMLElement, callback: MeasureCb) => Promise<LayoutValue | null> {\n return (relativeTo, callback) => measureLayout(node, relativeTo, callback)\n}\n"
|
|
27
29
|
],
|
|
28
30
|
"version": 3
|
|
29
31
|
}
|