view-anchor 0.2.0 → 0.2.1

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,4 +1,7 @@
1
1
  const ZERO = { x: 0, y: 0, width: 0, height: 0 };
2
+ // Replaces a disposed instance's publish callback so a retained handle does
3
+ // not keep the caller's original callback (and whatever it captured) alive.
4
+ const NOOP_PUBLISH = () => false;
2
5
  // Round to integer pixels. Width and height are clamped to >= 0 (0 represents
3
6
  // a collapsed rect). Coordinates (x, y) can be negative when an element is
4
7
  // scrolled out of view; clamping them to 0 would pin the view to the screen edge.
@@ -27,13 +30,16 @@ export function createViewAnchor(target, opts) {
27
30
  let present = opts.present;
28
31
  let publish = opts.publish;
29
32
  let observer = null;
33
+ // Local, clearable alias for the `target` parameter so dispose() can drop
34
+ // the strong reference without widening the public parameter's type.
35
+ let targetRef = target;
30
36
  // Last rect sent to publish. Reset on apply() so state changes (such as zoom)
31
37
  // force a re-publish even if the geometry did not change.
32
38
  let lastPublished = null;
33
39
  let publicationRevision = 0;
34
40
  let disposed = false;
35
41
  const measure = () => {
36
- const r = target.getBoundingClientRect();
42
+ const r = targetRef.getBoundingClientRect();
37
43
  // Drop ticks with non-finite values (NaN / Infinity cannot be sent over IPC).
38
44
  if (!Number.isFinite(r.left) ||
39
45
  !Number.isFinite(r.top) ||
@@ -49,12 +55,14 @@ export function createViewAnchor(target, opts) {
49
55
  lastPublished = candidate;
50
56
  try {
51
57
  const accepted = publish(candidate) !== false;
52
- if (!accepted && publicationRevision === attempt)
58
+ // A reentrant dispose() during publish() already cleared lastPublished;
59
+ // do not resurrect the pre-dispose value over that terminal state.
60
+ if (!accepted && publicationRevision === attempt && !disposed)
53
61
  lastPublished = previous;
54
62
  return accepted;
55
63
  }
56
64
  catch (error) {
57
- if (publicationRevision === attempt)
65
+ if (publicationRevision === attempt && !disposed)
58
66
  lastPublished = previous;
59
67
  throw error;
60
68
  }
@@ -75,7 +83,7 @@ export function createViewAnchor(target, opts) {
75
83
  if (observer)
76
84
  return;
77
85
  observer = new ResizeObserver(emit);
78
- observer.observe(target);
86
+ observer.observe(targetRef);
79
87
  window.addEventListener('resize', emit);
80
88
  };
81
89
  const stopObserving = () => {
@@ -114,6 +122,9 @@ export function createViewAnchor(target, opts) {
114
122
  return;
115
123
  disposed = true;
116
124
  stopObserving();
125
+ targetRef = null;
126
+ publish = NOOP_PUBLISH;
127
+ lastPublished = null;
117
128
  },
118
129
  };
119
130
  }
@@ -148,6 +159,9 @@ export function createPlacementAnchor(target, opts) {
148
159
  let guardDisplayNone = opts.guardDisplayNone ?? false;
149
160
  let followScroll = opts.followScroll ?? false;
150
161
  let followGeometry = opts.followGeometry ?? false;
162
+ // Local, clearable alias for the `target` parameter so dispose() can drop
163
+ // the strong reference without widening the public parameter's type.
164
+ let targetRef = target;
151
165
  let observer = null;
152
166
  let io = null;
153
167
  let scrollListening = false;
@@ -171,16 +185,14 @@ export function createPlacementAnchor(target, opts) {
171
185
  let activePointerId = null;
172
186
  let sentinelDeadline = null;
173
187
  const computePlacement = () => {
174
- const p = measurePlacement(target);
188
+ const p = measurePlacement(targetRef);
175
189
  if (p.visible &&
176
190
  (!Number.isFinite(p.bounds.x) ||
177
191
  !Number.isFinite(p.bounds.y) ||
178
192
  !Number.isFinite(p.bounds.width) ||
179
193
  !Number.isFinite(p.bounds.height)))
180
194
  return null;
181
- if (guardDisplayNone &&
182
- p.visible &&
183
- (p.bounds.width === 0 || p.bounds.height === 0)) {
195
+ if (guardDisplayNone && p.visible && (p.bounds.width === 0 || p.bounds.height === 0)) {
184
196
  return { visible: false };
185
197
  }
186
198
  return p;
@@ -191,12 +203,14 @@ export function createPlacementAnchor(target, opts) {
191
203
  lastPublished = candidate;
192
204
  try {
193
205
  const accepted = publish(candidate) !== false;
194
- if (!accepted && publicationRevision === attempt)
206
+ // A reentrant dispose() during publish() already cleared lastPublished;
207
+ // do not resurrect the pre-dispose value over that terminal state.
208
+ if (!accepted && publicationRevision === attempt && !disposed)
195
209
  lastPublished = previous;
196
210
  return accepted;
197
211
  }
198
212
  catch (error) {
199
- if (publicationRevision === attempt)
213
+ if (publicationRevision === attempt && !disposed)
200
214
  lastPublished = previous;
201
215
  throw error;
202
216
  }
@@ -328,7 +342,7 @@ export function createPlacementAnchor(target, opts) {
328
342
  const startOptionalObserving = () => {
329
343
  if (guardDisplayNone && !io && typeof IntersectionObserver !== 'undefined') {
330
344
  io = new IntersectionObserver(emit);
331
- io.observe(target);
345
+ io.observe(targetRef);
332
346
  }
333
347
  if (followScroll && !scrollListening) {
334
348
  window.addEventListener('scroll', onScroll, passiveCapture);
@@ -382,7 +396,7 @@ export function createPlacementAnchor(target, opts) {
382
396
  if (observer)
383
397
  return;
384
398
  observer = new ResizeObserver(emit);
385
- observer.observe(target);
399
+ observer.observe(targetRef);
386
400
  window.addEventListener('resize', emit);
387
401
  startOptionalObserving();
388
402
  };
@@ -431,6 +445,9 @@ export function createPlacementAnchor(target, opts) {
431
445
  return;
432
446
  disposed = true;
433
447
  stopObserving();
448
+ targetRef = null;
449
+ publish = NOOP_PUBLISH;
450
+ lastPublished = null;
434
451
  },
435
452
  pulse(durationMs) {
436
453
  if (disposed || !followGeometry)
@@ -79,21 +79,25 @@ export function createSizeAdvertiser(
79
79
  - 下游的 `createSizeAdvertiser` 将内容尺寸通知宿主,宿主更新占位元素的高度。
80
80
  - 宿主的 `createViewAnchor` 监听占位元素的矩形变化,将新位置同步给外部视图。
81
81
 
82
- ```
83
- 宿主渲染进程 宿主主进程 下游渲染进程(如工具栏页面)
84
- ────────── ──────── ────────────────────────────
85
- [占位 div] [内容容器(自适应高度)]
86
- │ ▲ │
87
- │ ② 宿主校验并更新占位高度 │ ① createSizeAdvertiser
88
- │ │ div.style.height = clamp(size.extent) │ 测量容器高度
89
- │ └─────────── IPC / postMessage ◀───────────────────────┘ publish(size) ──▶
90
-
91
- │ ③ 占位尺寸变化,ResizeObserver 触发
92
- │ 测量占位新矩形 → publish(bounds)
93
-
94
- ──── IPC ──▶ ④ view.setBounds(bounds) ──▶ [WebContentsView / 原生视图]
95
- │ 视图尺寸更新,下游视口变化
96
- └──▶ 下游内容重新排版(单步收敛)
82
+ ```mermaid
83
+ flowchart LR
84
+ subgraph DOWN["下游渲染进程(如工具栏页面)"]
85
+ C["内容容器<br/>高度由自身内容决定"]
86
+ end
87
+ subgraph HOST["宿主渲染进程"]
88
+ H["宿主消息处理器<br/>校验来源、clamp 数值"]
89
+ DIV["占位 div"]
90
+ VA["createViewAnchor"]
91
+ end
92
+ subgraph MAIN["宿主主进程"]
93
+ NV["WebContentsView / 原生视图"]
94
+ end
95
+
96
+ C -->|"① publish(size)"| H
97
+ H -->|"② clamp 后写入 style.height"| DIV
98
+ DIV -->|"ResizeObserver 观测"| VA
99
+ VA -->|"③ 测出新矩形,publish(bounds)"| NV
100
+ NV -->|"④ setBounds 后下游视口变化,内容重排"| C
97
101
  ```
98
102
 
99
103
  1. **下游**:通过 `createSizeAdvertiser` 测量高度并通过 IPC 发给宿主。