view-anchor 0.2.0 → 0.2.2-alpha.0
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/README.md +34 -44
- package/README.zh-CN.md +32 -44
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/measure-loop.d.ts.map +1 -1
- package/dist/measure-loop.js +22 -3
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +19 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -5
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -6
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +29 -12
- package/docs/bidirectional-design.md +19 -15
- package/docs/index.html +413 -398
- package/docs/{mechanism.mdx → mechanism.md} +1 -10
- package/docs/performance-report.md +30 -30
- package/docs/protocol.md +31 -2
- package/package.json +15 -16
- package/src/index.ts +2 -9
- package/src/measure-loop.ts +22 -3
- package/src/protocol-publisher.ts +21 -3
- package/src/protocol.ts +8 -20
- package/src/react.ts +4 -18
- package/src/size-advertiser.ts +9 -0
- package/src/types.ts +1 -3
- package/src/view-anchor.ts +41 -34
package/src/view-anchor.ts
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Bounds,
|
|
3
|
-
Placement,
|
|
4
|
-
Publisher,
|
|
5
|
-
ViewAnchorOptions,
|
|
6
|
-
ViewAnchorHandle,
|
|
7
|
-
} from './types.js'
|
|
1
|
+
import type { Bounds, Placement, Publisher, ViewAnchorOptions, ViewAnchorHandle } from './types.js'
|
|
8
2
|
|
|
9
3
|
const ZERO: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
|
10
4
|
|
|
5
|
+
// Replaces a disposed instance's publish callback so a retained handle does
|
|
6
|
+
// not keep the caller's original callback (and whatever it captured) alive.
|
|
7
|
+
const NOOP_PUBLISH = (): false => false
|
|
8
|
+
|
|
11
9
|
// Round to integer pixels. Width and height are clamped to >= 0 (0 represents
|
|
12
10
|
// a collapsed rect). Coordinates (x, y) can be negative when an element is
|
|
13
11
|
// scrolled out of view; clamping them to 0 would pin the view to the screen edge.
|
|
14
|
-
const clampRect = (r: {
|
|
15
|
-
x: number
|
|
16
|
-
y: number
|
|
17
|
-
width: number
|
|
18
|
-
height: number
|
|
19
|
-
}): Bounds => ({
|
|
12
|
+
const clampRect = (r: { x: number; y: number; width: number; height: number }): Bounds => ({
|
|
20
13
|
x: Math.round(r.x),
|
|
21
14
|
y: Math.round(r.y),
|
|
22
15
|
width: Math.max(0, Math.round(r.width)),
|
|
@@ -38,13 +31,13 @@ const clampRect = (r: {
|
|
|
38
31
|
* adding requestAnimationFrame would add a second frame of visual lag during drag
|
|
39
32
|
* operations. High-frequency updates are deduplicated against the last accepted rect.
|
|
40
33
|
*/
|
|
41
|
-
export function createViewAnchor(
|
|
42
|
-
target: HTMLElement,
|
|
43
|
-
opts: ViewAnchorOptions,
|
|
44
|
-
): ViewAnchorHandle {
|
|
34
|
+
export function createViewAnchor(target: HTMLElement, opts: ViewAnchorOptions): ViewAnchorHandle {
|
|
45
35
|
let present = opts.present
|
|
46
36
|
let publish = opts.publish
|
|
47
37
|
let observer: ResizeObserver | null = null
|
|
38
|
+
// Local, clearable alias for the `target` parameter so dispose() can drop
|
|
39
|
+
// the strong reference without widening the public parameter's type.
|
|
40
|
+
let targetRef: HTMLElement | null = target
|
|
48
41
|
// Last rect sent to publish. Reset on apply() so state changes (such as zoom)
|
|
49
42
|
// force a re-publish even if the geometry did not change.
|
|
50
43
|
let lastPublished: Bounds | null = null
|
|
@@ -52,14 +45,15 @@ export function createViewAnchor(
|
|
|
52
45
|
let disposed = false
|
|
53
46
|
|
|
54
47
|
const measure = (): Bounds | null => {
|
|
55
|
-
const r =
|
|
48
|
+
const r = targetRef!.getBoundingClientRect()
|
|
56
49
|
// Drop ticks with non-finite values (NaN / Infinity cannot be sent over IPC).
|
|
57
50
|
if (
|
|
58
51
|
!Number.isFinite(r.left) ||
|
|
59
52
|
!Number.isFinite(r.top) ||
|
|
60
53
|
!Number.isFinite(r.width) ||
|
|
61
54
|
!Number.isFinite(r.height)
|
|
62
|
-
)
|
|
55
|
+
)
|
|
56
|
+
return null
|
|
63
57
|
return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height })
|
|
64
58
|
}
|
|
65
59
|
|
|
@@ -72,10 +66,12 @@ export function createViewAnchor(
|
|
|
72
66
|
lastPublished = candidate
|
|
73
67
|
try {
|
|
74
68
|
const accepted = publish(candidate) !== false
|
|
75
|
-
|
|
69
|
+
// A reentrant dispose() during publish() already cleared lastPublished;
|
|
70
|
+
// do not resurrect the pre-dispose value over that terminal state.
|
|
71
|
+
if (!accepted && publicationRevision === attempt && !disposed) lastPublished = previous
|
|
76
72
|
return accepted
|
|
77
73
|
} catch (error) {
|
|
78
|
-
if (publicationRevision === attempt) lastPublished = previous
|
|
74
|
+
if (publicationRevision === attempt && !disposed) lastPublished = previous
|
|
79
75
|
throw error
|
|
80
76
|
}
|
|
81
77
|
}
|
|
@@ -93,7 +89,7 @@ export function createViewAnchor(
|
|
|
93
89
|
const startObserving = (): void => {
|
|
94
90
|
if (observer) return
|
|
95
91
|
observer = new ResizeObserver(emit)
|
|
96
|
-
observer.observe(
|
|
92
|
+
observer.observe(targetRef!)
|
|
97
93
|
window.addEventListener('resize', emit)
|
|
98
94
|
}
|
|
99
95
|
|
|
@@ -132,6 +128,9 @@ export function createViewAnchor(
|
|
|
132
128
|
if (disposed) return
|
|
133
129
|
disposed = true
|
|
134
130
|
stopObserving()
|
|
131
|
+
targetRef = null
|
|
132
|
+
publish = NOOP_PUBLISH
|
|
133
|
+
lastPublished = null
|
|
135
134
|
},
|
|
136
135
|
}
|
|
137
136
|
}
|
|
@@ -220,6 +219,9 @@ export function createPlacementAnchor(
|
|
|
220
219
|
let guardDisplayNone = opts.guardDisplayNone ?? false
|
|
221
220
|
let followScroll = opts.followScroll ?? false
|
|
222
221
|
let followGeometry = opts.followGeometry ?? false
|
|
222
|
+
// Local, clearable alias for the `target` parameter so dispose() can drop
|
|
223
|
+
// the strong reference without widening the public parameter's type.
|
|
224
|
+
let targetRef: HTMLElement | null = target
|
|
223
225
|
let observer: ResizeObserver | null = null
|
|
224
226
|
let io: IntersectionObserver | null = null
|
|
225
227
|
let scrollListening = false
|
|
@@ -245,19 +247,16 @@ export function createPlacementAnchor(
|
|
|
245
247
|
let sentinelDeadline: number | null = null
|
|
246
248
|
|
|
247
249
|
const computePlacement = (): Placement | null => {
|
|
248
|
-
const p = measurePlacement(
|
|
250
|
+
const p = measurePlacement(targetRef!)
|
|
249
251
|
if (
|
|
250
252
|
p.visible &&
|
|
251
253
|
(!Number.isFinite(p.bounds.x) ||
|
|
252
254
|
!Number.isFinite(p.bounds.y) ||
|
|
253
255
|
!Number.isFinite(p.bounds.width) ||
|
|
254
256
|
!Number.isFinite(p.bounds.height))
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
p.visible &&
|
|
259
|
-
(p.bounds.width === 0 || p.bounds.height === 0)
|
|
260
|
-
) {
|
|
257
|
+
)
|
|
258
|
+
return null
|
|
259
|
+
if (guardDisplayNone && p.visible && (p.bounds.width === 0 || p.bounds.height === 0)) {
|
|
261
260
|
return { visible: false }
|
|
262
261
|
}
|
|
263
262
|
return p
|
|
@@ -269,10 +268,12 @@ export function createPlacementAnchor(
|
|
|
269
268
|
lastPublished = candidate
|
|
270
269
|
try {
|
|
271
270
|
const accepted = publish(candidate) !== false
|
|
272
|
-
|
|
271
|
+
// A reentrant dispose() during publish() already cleared lastPublished;
|
|
272
|
+
// do not resurrect the pre-dispose value over that terminal state.
|
|
273
|
+
if (!accepted && publicationRevision === attempt && !disposed) lastPublished = previous
|
|
273
274
|
return accepted
|
|
274
275
|
} catch (error) {
|
|
275
|
-
if (publicationRevision === attempt) lastPublished = previous
|
|
276
|
+
if (publicationRevision === attempt && !disposed) lastPublished = previous
|
|
276
277
|
throw error
|
|
277
278
|
}
|
|
278
279
|
}
|
|
@@ -315,7 +316,10 @@ export function createPlacementAnchor(
|
|
|
315
316
|
}
|
|
316
317
|
invalidFrames = 0
|
|
317
318
|
if (!p.visible) {
|
|
318
|
-
if (shouldCloseOnHiddenPoll()) {
|
|
319
|
+
if (shouldCloseOnHiddenPoll()) {
|
|
320
|
+
sentinelDeadline = null
|
|
321
|
+
return
|
|
322
|
+
}
|
|
319
323
|
if (!disposed && visible && followGeometry) {
|
|
320
324
|
rafId = requestAnimationFrame(sentinelFrame)
|
|
321
325
|
} else {
|
|
@@ -398,7 +402,7 @@ export function createPlacementAnchor(
|
|
|
398
402
|
const startOptionalObserving = (): void => {
|
|
399
403
|
if (guardDisplayNone && !io && typeof IntersectionObserver !== 'undefined') {
|
|
400
404
|
io = new IntersectionObserver(emit)
|
|
401
|
-
io.observe(
|
|
405
|
+
io.observe(targetRef!)
|
|
402
406
|
}
|
|
403
407
|
if (followScroll && !scrollListening) {
|
|
404
408
|
window.addEventListener('scroll', onScroll, passiveCapture)
|
|
@@ -454,7 +458,7 @@ export function createPlacementAnchor(
|
|
|
454
458
|
const startObserving = (): void => {
|
|
455
459
|
if (observer) return
|
|
456
460
|
observer = new ResizeObserver(emit)
|
|
457
|
-
observer.observe(
|
|
461
|
+
observer.observe(targetRef!)
|
|
458
462
|
window.addEventListener('resize', emit)
|
|
459
463
|
startOptionalObserving()
|
|
460
464
|
}
|
|
@@ -503,6 +507,9 @@ export function createPlacementAnchor(
|
|
|
503
507
|
if (disposed) return
|
|
504
508
|
disposed = true
|
|
505
509
|
stopObserving()
|
|
510
|
+
targetRef = null
|
|
511
|
+
publish = NOOP_PUBLISH
|
|
512
|
+
lastPublished = null
|
|
506
513
|
},
|
|
507
514
|
pulse(durationMs?: number): void {
|
|
508
515
|
if (disposed || !followGeometry) return
|