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.
@@ -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 = target.getBoundingClientRect()
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
- ) return null
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
- if (!accepted && publicationRevision === attempt) lastPublished = previous
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(target)
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(target)
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
- ) return null
256
- if (
257
- guardDisplayNone &&
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
- if (!accepted && publicationRevision === attempt) lastPublished = previous
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()) { sentinelDeadline = null; return }
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(target)
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(target)
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