view-anchor 0.2.2-alpha.0 → 0.2.2
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 +55 -25
- package/README.zh-CN.md +90 -77
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +9 -0
- package/dist/protocol-publisher.d.ts +4 -2
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +25 -14
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +26 -16
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts +4 -2
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +42 -23
- package/docs/bidirectional-design.md +21 -32
- package/docs/index.html +54 -28
- package/docs/mechanism.md +22 -22
- package/docs/performance-report.md +2 -2
- package/docs/protocol.md +7 -7
- package/package.json +3 -11
- package/src/abort.ts +9 -0
- package/src/protocol-publisher.ts +26 -14
- package/src/size-advertiser.ts +26 -15
- package/src/types.ts +4 -0
- package/src/view-anchor.ts +44 -21
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — keep anything outside the DOM aligned to a DOM element" width="820">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> Keep an external surface aligned to a DOM element. `view-anchor` measures the element and synchronously gives your code the current geometry whenever it changes.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/view-anchor)
|
|
8
8
|
[](https://www.npmjs.com/package/view-anchor)
|
|
@@ -15,25 +15,35 @@
|
|
|
15
15
|
|
|
16
16
|
## The problem
|
|
17
17
|
|
|
18
|
-
Some
|
|
18
|
+
Some surfaces are positioned by application code rather than by CSS. They may be a canvas, a video overlay, an embedded document, or any other thing you can position from a rectangle. Your layout moves DOM elements, but it cannot move that external surface by itself.
|
|
19
19
|
|
|
20
|
-
`view-anchor`
|
|
20
|
+
Point `view-anchor` at a placeholder element. It measures the element on creation, on `ResizeObserver` notifications, and on window `resize`, then calls your `publish` function. Your function applies, stores, or sends that value using the mechanism your application already has.
|
|
21
21
|
|
|
22
|
-
The core has no dependency on
|
|
22
|
+
The core has no dependency on a host runtime, React, or a layout library. It only uses `ResizeObserver`, `requestAnimationFrame`, and `getBoundingClientRect`. React support lives in a separate `view-anchor/react` entry.
|
|
23
|
+
|
|
24
|
+
## What `publish` receives
|
|
25
|
+
|
|
26
|
+
`publish` is not a built-in transport. It is a synchronous function that the library calls with one of these plain values:
|
|
27
|
+
|
|
28
|
+
- `createViewAnchor` calls `publish(bounds)`, where `bounds` is `{ x, y, width, height }` in CSS pixels from `getBoundingClientRect()`. When `present` becomes `false`, it calls `publish({ x: 0, y: 0, width: 0, height: 0 })` once and stops observing.
|
|
29
|
+
- `createPlacementAnchor` calls `publish({ visible: true, bounds })` while shown, or `publish({ visible: false })` while hidden. Use it when a visible zero-sized element differs from a hidden one.
|
|
30
|
+
- `createSizeAdvertiser` calls `publish({ axis, extent })`, where `axis` is `block` (height) or `inline` (width), and `extent` is the rounded non-negative content size in CSS pixels.
|
|
31
|
+
|
|
32
|
+
Return `false` only when the value was not accepted; a later measurement may retry it. Return `true` or nothing after accepting or queueing it.
|
|
23
33
|
|
|
24
34
|
## Built for the hot path
|
|
25
35
|
|
|
26
36
|
Geometry updates fire on every resize and, when following a drag, on every animation frame. The library keeps that work predictable:
|
|
27
37
|
|
|
28
38
|
- **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
|
|
29
|
-
- **Dedupe
|
|
39
|
+
- **Dedupe outgoing updates.** A rectangle identical to the last accepted one is not passed to `publish` again.
|
|
30
40
|
- **Frame following only when needed.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then closes itself once the rectangle settles. Idle cost is zero, and hidden or invalid targets are capped at 30 frames.
|
|
31
41
|
- **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
|
|
32
42
|
- **Latest-wins batching.** Messages queued in the same task are merged in a microtask. The newest placement and size for each anchor are sent separately.
|
|
33
43
|
- **Release on disposal.** Disposed handles stop observing and release their target and callback references, even when the caller keeps the handle.
|
|
34
44
|
- **Compact, tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
|
|
35
45
|
|
|
36
|
-
The [performance report](./docs/performance-report.md) contains the reproducible measurements and their environment. They are useful for comparing changes on the same machine, not for predicting
|
|
46
|
+
The [performance report](./docs/performance-report.md) contains the reproducible measurements and their environment. They are useful for comparing changes on the same machine, not for predicting browser layout, serialization, delivery, or your app's workload.
|
|
37
47
|
|
|
38
48
|
## Installation
|
|
39
49
|
|
|
@@ -52,16 +62,31 @@ React is an optional peer dependency. Import the hooks from `view-anchor/react`.
|
|
|
52
62
|
```ts
|
|
53
63
|
import { createViewAnchor } from 'view-anchor'
|
|
54
64
|
|
|
65
|
+
const publish = (bounds) => {
|
|
66
|
+
applyBounds(bounds)
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
const handle = createViewAnchor(target, {
|
|
56
|
-
present: true,
|
|
57
|
-
publish
|
|
70
|
+
present: true,
|
|
71
|
+
publish,
|
|
58
72
|
})
|
|
59
73
|
|
|
60
|
-
handle.update({ present, publish }) // apply new options and publish right away
|
|
61
|
-
handle.dispose()
|
|
74
|
+
handle.update({ present: true, publish }) // apply new options and publish right away
|
|
75
|
+
handle.dispose() // stop observing; never publishes again
|
|
62
76
|
```
|
|
63
77
|
|
|
64
|
-
Set `present: false` to collapse the
|
|
78
|
+
Set `present: false` to collapse the surface. The core publishes a zero rectangle and stops observing. Your `publish` function decides whether that removes, hides, or retains the external surface.
|
|
79
|
+
|
|
80
|
+
For ancestor scroll, transforms, or other position-only changes, use `createPlacementAnchor` with `followScroll` or `followGeometry` as needed.
|
|
81
|
+
|
|
82
|
+
`createViewAnchor`, `createPlacementAnchor`, `createSizeAdvertiser`, and `createGeometryBatcher` accept `signal`. Aborting it is equivalent to `dispose()`; an already-aborted signal does not measure, publish, or install listeners.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const controller = new AbortController()
|
|
86
|
+
const handle = createViewAnchor(target, { present: true, publish, signal: controller.signal })
|
|
87
|
+
|
|
88
|
+
controller.abort() // same cleanup as handle.dispose()
|
|
89
|
+
```
|
|
65
90
|
|
|
66
91
|
### React
|
|
67
92
|
|
|
@@ -73,8 +98,8 @@ function DebugPanel({ visible }: { visible: boolean }) {
|
|
|
73
98
|
present: visible,
|
|
74
99
|
publish: publishPanelBounds,
|
|
75
100
|
})
|
|
76
|
-
// The
|
|
77
|
-
//
|
|
101
|
+
// The external surface follows this placeholder. Hiding or unmounting it
|
|
102
|
+
// sends the collapsed value, without deciding how the surface is stored.
|
|
78
103
|
return <div ref={ref} className="h-full w-full" />
|
|
79
104
|
}
|
|
80
105
|
```
|
|
@@ -89,9 +114,12 @@ A zero rectangle cannot tell a hidden view from one that is visible but currentl
|
|
|
89
114
|
import { createPlacementAnchor } from 'view-anchor'
|
|
90
115
|
|
|
91
116
|
const handle = createPlacementAnchor(target, {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
visible: true,
|
|
118
|
+
publish(placement) {
|
|
119
|
+
applyPlacement(placement)
|
|
120
|
+
},
|
|
121
|
+
followScroll: true, // re-measure when any ancestor scrolls
|
|
122
|
+
followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
|
|
95
123
|
guardDisplayNone: true, // zero-area or display:none target → { visible: false }
|
|
96
124
|
})
|
|
97
125
|
|
|
@@ -108,19 +136,21 @@ Sometimes the hosted surface's size should come from its own content, for exampl
|
|
|
108
136
|
import { createSizeAdvertiser } from 'view-anchor'
|
|
109
137
|
|
|
110
138
|
const handle = createSizeAdvertiser(contentWrapper, {
|
|
111
|
-
axis: 'block',
|
|
112
|
-
publish
|
|
139
|
+
axis: 'block', // one axis per advertiser: block = height, inline = width
|
|
140
|
+
publish(size) {
|
|
141
|
+
updatePlaceholderSize(size)
|
|
142
|
+
},
|
|
113
143
|
})
|
|
114
144
|
|
|
115
145
|
handle.update(publish) // swap the publish channel and report the current size again
|
|
116
|
-
handle.dispose()
|
|
146
|
+
handle.dispose() // stop observing; never reports again
|
|
117
147
|
```
|
|
118
148
|
|
|
119
149
|
> **Warning:** the target must shrink to fit its content on the owned axis. If the host sets that size instead, the two sides keep reacting to each other and never settle. See [docs/bidirectional-design.md](./docs/bidirectional-design.md).
|
|
120
150
|
|
|
121
151
|
### Versioned transport across a boundary
|
|
122
152
|
|
|
123
|
-
The core hands you plain `Bounds`, `Placement`, and `AdvertisedSize` values.
|
|
153
|
+
The core hands you plain `Bounds`, `Placement`, and `AdvertisedSize` values. If an application passes them through an asynchronous or untrusted channel, it usually needs validation and ordering. The optional `view-anchor/protocol` entry adds versioned message envelopes, bounded decoding, a per-anchor sequence guard that drops stale messages, and a microtask batcher:
|
|
124
154
|
|
|
125
155
|
```ts
|
|
126
156
|
import {
|
|
@@ -129,14 +159,14 @@ import {
|
|
|
129
159
|
decodeGeometryWireValue,
|
|
130
160
|
} from 'view-anchor/protocol'
|
|
131
161
|
|
|
132
|
-
//
|
|
133
|
-
const batcher = createGeometryBatcher(
|
|
162
|
+
// `sendGeometryBatch` is supplied by your application.
|
|
163
|
+
const batcher = createGeometryBatcher(sendGeometryBatch)
|
|
134
164
|
const publish = createPlacementMessagePublisher(
|
|
135
165
|
{ anchorId: 'editor', generation: 3 },
|
|
136
166
|
batcher.publish,
|
|
137
167
|
)
|
|
138
168
|
|
|
139
|
-
// receiving side
|
|
169
|
+
// receiving side
|
|
140
170
|
const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
|
|
141
171
|
if (decoded.ok) {
|
|
142
172
|
/* check the sender, then apply only newer messages */
|
|
@@ -154,10 +184,10 @@ The full contract is in [docs/protocol.md](./docs/protocol.md).
|
|
|
154
184
|
|
|
155
185
|
| Export | Kind | Purpose |
|
|
156
186
|
| --------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
-
| `createViewAnchor(target, opts)` | function | Measure a DOM element and publish
|
|
187
|
+
| `createViewAnchor(target, opts)` | function | Measure a DOM element and call `publish({ x, y, width, height })`. A zero rect means collapsed. |
|
|
158
188
|
| `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, opt-in `followScroll` / `followGeometry` / `guardDisplayNone`, and `pulse()`. |
|
|
159
189
|
| `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
|
|
160
|
-
| `createSizeAdvertiser(target, opts)` | function |
|
|
190
|
+
| `createSizeAdvertiser(target, opts)` | function | Call `publish({ axis, extent })` with one content-size axis. |
|
|
161
191
|
| `useViewAnchor(opts)` from `view-anchor/react` | hook | Returns a ref callback for a placeholder element. |
|
|
162
192
|
| `usePlacementAnchor(opts)` from `view-anchor/react` | hook | React adapter for the `Placement` API, including `followScroll` and `followGeometry`. |
|
|
163
193
|
| `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
|
package/README.zh-CN.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — 让 DOM
|
|
2
|
+
<img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — 让 DOM 之外的画面实时对齐 DOM 元素" width="820">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> 让 DOM 之外的画面实时对齐 DOM 元素:测量占位元素,逐帧同步最新矩形,拖拽无延迟。
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/view-anchor)
|
|
8
8
|
[](https://www.npmjs.com/package/view-anchor)
|
|
@@ -11,29 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
13
13
|
|
|
14
|
-
>
|
|
14
|
+
> [在线演示](https://lbb00.github.io/view-anchor/):拖动分栏、切换面板或 3D 视角,查看外部画面如何跟随占位元素。
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## 它解决什么问题
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
有些画面的位置不是由 CSS 直接控制:例如应用自己摆放的叠层、嵌入式文档,或能根据矩形定位的渲染表面。DOM 布局会移动占位元素,却不会自动更新这些画面的位置。
|
|
19
19
|
|
|
20
|
-
`view-anchor`
|
|
20
|
+
`view-anchor` 负责测量占位元素。元素尺寸变化或窗口大小变化时,它同步调用你提供的 `publish` 函数。函数怎么使用这个值由应用决定。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
## `publish` 收到什么
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
`publish` 不是内置传输通道,而是一个同步回调。不同接口会传入不同的普通对象:
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
- `createViewAnchor` 调用 `publish(bounds)`。`bounds` 是相对视口、取整后的 CSS 像素矩形:`{ x, y, width, height }`。`present` 变为 `false` 时,会发布一次全零矩形,然后停止观察。
|
|
27
|
+
- `createPlacementAnchor` 在显示时调用 `publish({ visible: true, bounds })`,隐藏时调用 `publish({ visible: false })`。
|
|
28
|
+
- `createSizeAdvertiser` 调用 `publish({ axis, extent })`。`axis` 为 `block`(高度)或 `inline`(宽度),`extent` 是取整后且不小于零的内容尺寸。
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
- **先去重,再分配。** 和上一次已接受的矩形完全相同的结果,只比较四个数字就会被丢弃,不会创建任何对象。
|
|
30
|
-
- **只在需要时逐帧跟随。** `followGeometry` 只在滚动、拖动分栏或显式调用 `pulse()` 时才开始按 `requestAnimationFrame` 轮询,矩形稳定后自动停止。空闲时开销为零,隐藏或无效目标最多跟 30 帧。
|
|
31
|
-
- **generation 切换是 O(1)。** 协议层里,把某个锚点换到新的 generation 或清除它,不会碰到其他锚点。
|
|
32
|
-
- **合并批量,只发最新。** 同一个任务里排队的消息在一个微任务里合并,每个锚点分别发送最新的位置和尺寸。
|
|
33
|
-
- **销毁后释放引用。** 句柄 `dispose()` 后会停止观察,并释放目标元素和回调;调用方继续持有句柄也不会保留它们。
|
|
34
|
-
- **核心紧凑,可摇树。** 每个函数都是独立导出,并声明了 `sideEffects: false`;完整核心导出 gzip 后小于 3 KB。
|
|
35
|
-
|
|
36
|
-
[性能报告](./docs/performance-report.md) 保留可复现的测量数据和环境。它适合比较同一台机器上的改动,不用于预测 DOM layout、Electron IPC、structured clone 或你的应用负载。
|
|
30
|
+
数据已接收或已经入队时,返回 `true` 或不返回值;当前不能接收时才返回 `false`。下一次测量会重试被拒绝的值。
|
|
37
31
|
|
|
38
32
|
## 安装
|
|
39
33
|
|
|
@@ -43,84 +37,103 @@ pnpm add view-anchor
|
|
|
43
37
|
npm install view-anchor
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
React
|
|
40
|
+
React 是可选 peer dependency。Hook 从 `view-anchor/react` 导入;根入口为了兼容 `v0.1.2` 也会导出 `useViewAnchor`,因此导入根入口时需要安装 React。`view-anchor/protocol` 不需要 React。
|
|
47
41
|
|
|
48
|
-
##
|
|
42
|
+
## 使用
|
|
49
43
|
|
|
50
44
|
### 跟随一个 DOM 元素
|
|
51
45
|
|
|
52
46
|
```ts
|
|
53
47
|
import { createViewAnchor } from 'view-anchor'
|
|
54
48
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
const publish = (bounds) => {
|
|
50
|
+
applyBoundsToExternalSurface(bounds)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handle = createViewAnchor(placeholderEl, {
|
|
54
|
+
present: true,
|
|
55
|
+
publish,
|
|
58
56
|
})
|
|
59
57
|
|
|
60
|
-
handle.update({ present, publish }) //
|
|
61
|
-
handle.dispose()
|
|
58
|
+
handle.update({ present: true, publish }) // 更新选项并立即重新发布
|
|
59
|
+
handle.dispose() // 停止观察;之后不会再发布
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
默认的 `createViewAnchor` 监听 `ResizeObserver` 和窗口 `resize`。如果祖先滚动、transform 或拖拽会移动元素而不改变它自身尺寸,请使用下面的 `createPlacementAnchor`,按需开启 `followScroll` 或 `followGeometry`。
|
|
63
|
+
|
|
64
|
+
把 `present` 设为 `false` 会发布全零矩形并停止观察。你的 `publish` 函数可以把它解释为隐藏、移除,或保留外部画面。
|
|
65
|
+
|
|
66
|
+
`createViewAnchor`、`createPlacementAnchor`、`createSizeAdvertiser` 和 `createGeometryBatcher` 都支持 `signal`。调用 `AbortController.abort()` 的清理效果等同于 `dispose()`;创建前已经 abort 的 signal 不会开始测量或安装监听。
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
const handle = createViewAnchor(placeholderEl, {
|
|
71
|
+
present: true,
|
|
72
|
+
publish,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
controller.abort()
|
|
77
|
+
```
|
|
65
78
|
|
|
66
79
|
### React
|
|
67
80
|
|
|
68
81
|
```tsx
|
|
69
82
|
import { useViewAnchor } from 'view-anchor/react'
|
|
70
83
|
|
|
71
|
-
function
|
|
84
|
+
function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
|
|
72
85
|
const ref = useViewAnchor({
|
|
73
86
|
present: visible,
|
|
74
|
-
publish:
|
|
87
|
+
publish: applyBoundsToExternalSurface,
|
|
75
88
|
})
|
|
76
|
-
|
|
77
|
-
// 只会收起视图,不会销毁它。
|
|
89
|
+
|
|
78
90
|
return <div ref={ref} className="h-full w-full" />
|
|
79
91
|
}
|
|
80
92
|
```
|
|
81
93
|
|
|
82
|
-
Hook
|
|
94
|
+
元素卸载时,Hook 会先发布收起值,再释放监听。它兼容 React 18 和 19 的 StrictMode 重挂载。
|
|
83
95
|
|
|
84
96
|
### 显式可见性
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
全零矩形无法区分“已隐藏”和“可见但恰好是 0×0”。需要这个区分时使用 `Placement` API:
|
|
87
99
|
|
|
88
100
|
```ts
|
|
89
101
|
import { createPlacementAnchor } from 'view-anchor'
|
|
90
102
|
|
|
91
103
|
const handle = createPlacementAnchor(target, {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
visible: true,
|
|
105
|
+
publish: applyPlacement,
|
|
106
|
+
followScroll: true, // 祖先容器滚动时重新测量
|
|
107
|
+
followGeometry: true, // 拖拽或滚动期间逐帧测量;稳定后停止
|
|
108
|
+
guardDisplayNone: true, // 零面积或 display:none 时发布 { visible: false }
|
|
96
109
|
})
|
|
97
110
|
|
|
98
|
-
handle.pulse() //
|
|
111
|
+
handle.pulse() // 例如在 CSS 过渡期间主动打开一段逐帧测量
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
React
|
|
114
|
+
React 版本为 `view-anchor/react` 导出的 `usePlacementAnchor`。
|
|
102
115
|
|
|
103
|
-
###
|
|
116
|
+
### 由内容决定占位尺寸
|
|
104
117
|
|
|
105
|
-
|
|
118
|
+
当外部画面的内容尺寸需要反过来调整占位元素时,在内容所在的文档中使用 `createSizeAdvertiser`:
|
|
106
119
|
|
|
107
120
|
```ts
|
|
108
121
|
import { createSizeAdvertiser } from 'view-anchor'
|
|
109
122
|
|
|
110
123
|
const handle = createSizeAdvertiser(contentWrapper, {
|
|
111
|
-
axis: 'block',
|
|
112
|
-
publish:
|
|
124
|
+
axis: 'block', // 一个 advertiser 只负责一个轴:block = 高度,inline = 宽度
|
|
125
|
+
publish: updatePlaceholderSize,
|
|
113
126
|
})
|
|
114
127
|
|
|
115
|
-
handle.update(
|
|
116
|
-
handle.dispose()
|
|
128
|
+
handle.update(updatePlaceholderSize) // 有当前尺寸时会立即重新报告
|
|
129
|
+
handle.dispose()
|
|
117
130
|
```
|
|
118
131
|
|
|
119
|
-
|
|
132
|
+
目标元素在它负责的轴上必须随内容伸缩。若应用也反过来在同一轴上强制设置尺寸,双方会互相触发布局,难以稳定。详见 [双向几何设计](./docs/bidirectional-design.md)。
|
|
120
133
|
|
|
121
|
-
###
|
|
134
|
+
### 需要传递、校验和排序时
|
|
122
135
|
|
|
123
|
-
|
|
136
|
+
核心只交付 `Bounds`、`Placement` 和 `AdvertisedSize`。如果应用需要把这些值交给别的页面、环境或异步通道,可选的 `view-anchor/protocol` 提供消息版本、输入校验、乱序过滤和微任务合并。
|
|
124
137
|
|
|
125
138
|
```ts
|
|
126
139
|
import {
|
|
@@ -129,57 +142,57 @@ import {
|
|
|
129
142
|
decodeGeometryWireValue,
|
|
130
143
|
} from 'view-anchor/protocol'
|
|
131
144
|
|
|
132
|
-
//
|
|
133
|
-
const batcher = createGeometryBatcher(
|
|
145
|
+
// sendGeometryBatch 由应用实现:接收一个 batch,并同步返回是否已接收。
|
|
146
|
+
const batcher = createGeometryBatcher(sendGeometryBatch)
|
|
134
147
|
const publish = createPlacementMessagePublisher(
|
|
135
148
|
{ anchorId: 'editor', generation: 3 },
|
|
136
149
|
batcher.publish,
|
|
137
150
|
)
|
|
138
151
|
|
|
139
|
-
// 接收方(主进程、宿主页面等)
|
|
140
152
|
const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
|
|
141
153
|
if (decoded.ok) {
|
|
142
|
-
|
|
154
|
+
// 先验证来源,再只应用更新的消息。
|
|
143
155
|
}
|
|
144
156
|
```
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
同一个 `{ anchorId, generation }` 应只保留一个 publisher 实例。针对同一地址重新创建 publisher 会让序号回到 1,接收端会把新消息当作过期消息丢弃。需要重新开始时,把 `generation` 加一。
|
|
159
|
+
|
|
160
|
+
完整约定见 [通信协议](./docs/protocol.md)。
|
|
161
|
+
|
|
162
|
+
## 性能边界
|
|
147
163
|
|
|
148
|
-
-
|
|
149
|
-
-
|
|
164
|
+
- 相同的已发布矩形不会再次调用 `publish`。
|
|
165
|
+
- `followGeometry` 只在需要时启动动画帧轮询,稳定后停止。
|
|
166
|
+
- `createGeometryBatcher` 会合并同一微任务内的更新;每个锚点的位置和尺寸各保留最新值。
|
|
167
|
+
- `dispose()` 或 abort 会停止监听并释放长期持有的元素和回调引用。
|
|
150
168
|
|
|
151
|
-
|
|
169
|
+
性能数据和测量方式见 [性能报告](./docs/performance-report.md)。这些数据只用于比较同一台机器上的改动,不能代表浏览器布局、传递或应用业务的耗时。
|
|
152
170
|
|
|
153
171
|
## API
|
|
154
172
|
|
|
155
|
-
| 导出
|
|
156
|
-
|
|
|
157
|
-
| `createViewAnchor(target, opts)`
|
|
158
|
-
| `createPlacementAnchor(target, opts)`
|
|
159
|
-
| `measurePlacement(target)`
|
|
160
|
-
| `createSizeAdvertiser(target, opts)`
|
|
161
|
-
| `useViewAnchor(opts)
|
|
162
|
-
| `usePlacementAnchor(opts)
|
|
163
|
-
| `Bounds`
|
|
164
|
-
| `Placement`
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `UseViewAnchorOptions` / `ViewAnchorRef`,来自 `view-anchor/react` | 类型 | `useViewAnchor` 的选项和 ref 形状。 |
|
|
168
|
-
| `UsePlacementAnchorOptions` / `PlacementAnchorRef`,来自 `view-anchor/react` | 类型 | `usePlacementAnchor` 的选项和 ref 形状。 |
|
|
169
|
-
| `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向方向的轴和负载类型。 |
|
|
170
|
-
| `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | `createSizeAdvertiser` 的选项和句柄。 |
|
|
171
|
-
| `view-anchor/protocol` | 函数 + 类型 | 带版本的消息、严格解码、序列守卫、消息 publisher 和微任务批处理。 |
|
|
173
|
+
| 导出 | 类型 | 用途 |
|
|
174
|
+
| ------------------------------------- | ----------- | -------------------------------------------------------------------------- |
|
|
175
|
+
| `createViewAnchor(target, opts)` | 函数 | 测量 DOM 元素并调用 `publish({ x, y, width, height })`。全零矩形表示收起。 |
|
|
176
|
+
| `createPlacementAnchor(target, opts)` | 函数 | 发布带可见性的 `Placement`,可选滚动和几何跟随。 |
|
|
177
|
+
| `measurePlacement(target)` | 函数 | 读取当前矩形,返回 `{ visible: true, bounds }`。 |
|
|
178
|
+
| `createSizeAdvertiser(target, opts)` | 函数 | 用 `publish({ axis, extent })` 上报一个内容尺寸轴。 |
|
|
179
|
+
| `useViewAnchor(opts)` | Hook | 返回占位元素的 ref 回调。 |
|
|
180
|
+
| `usePlacementAnchor(opts)` | Hook | `Placement` API 的 React 适配。 |
|
|
181
|
+
| `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
|
|
182
|
+
| `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
|
|
183
|
+
| `AdvertisedAxis` / `AdvertisedSize` | 类型 | 内容尺寸上报的轴和数据。 |
|
|
184
|
+
| `view-anchor/protocol` | 函数 + 类型 | 消息封装、校验、排序和批处理。 |
|
|
172
185
|
|
|
173
186
|
## 文档
|
|
174
187
|
|
|
175
|
-
- [
|
|
176
|
-
- [
|
|
177
|
-
- [
|
|
178
|
-
- [
|
|
188
|
+
- [机制说明](./docs/mechanism.md):正向测量的触发条件、收起和 React 生命周期。
|
|
189
|
+
- [双向几何设计](./docs/bidirectional-design.md):内容尺寸反向调整占位元素时的单轴约束。
|
|
190
|
+
- [通信协议](./docs/protocol.md):消息格式、校验、排序和批处理。
|
|
191
|
+
- [性能报告](./docs/performance-report.md):可复现的性能数据和测量环境。
|
|
179
192
|
|
|
180
193
|
## 参与贡献
|
|
181
194
|
|
|
182
|
-
|
|
195
|
+
提交前运行 `pnpm lint`、`pnpm format:check`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 用于更新性能报告。
|
|
183
196
|
|
|
184
197
|
## 许可证
|
|
185
198
|
|
package/dist/abort.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3F"}
|
package/dist/abort.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const NOOP = () => { };
|
|
2
|
+
/** Attach a one-shot abort listener and return a function that detaches it. */
|
|
3
|
+
export function watchAbort(signal, dispose) {
|
|
4
|
+
if (!signal)
|
|
5
|
+
return NOOP;
|
|
6
|
+
const onAbort = () => dispose();
|
|
7
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
8
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
9
|
+
}
|
|
@@ -5,6 +5,8 @@ export type GeometryBatchSend = Publisher<GeometryBatch>;
|
|
|
5
5
|
export interface GeometryBatcherOptions {
|
|
6
6
|
/** Observes every batch-delivery error, including explicit flushes; it must not throw. */
|
|
7
7
|
onError?: (error: unknown) => void;
|
|
8
|
+
/** Stops this batcher when aborted. An already-aborted signal starts no work. */
|
|
9
|
+
signal?: AbortSignal;
|
|
8
10
|
}
|
|
9
11
|
export interface GeometryBatcher {
|
|
10
12
|
/** Queues one message. Returns false after disposal. */
|
|
@@ -34,8 +36,8 @@ export declare function createPlacementMessagePublisher(address: GeometryAddress
|
|
|
34
36
|
export declare function createSizeMessagePublisher(address: GeometryAddress, send: GeometrySend): (size: AdvertisedSize) => boolean;
|
|
35
37
|
/**
|
|
36
38
|
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
37
|
-
* no authorization policy: callers must associate addresses with trusted
|
|
38
|
-
*
|
|
39
|
+
* no authorization policy: callers must associate addresses with trusted
|
|
40
|
+
* sources before accepting a delivered batch.
|
|
39
41
|
*/
|
|
40
42
|
export declare function createGeometryBatcher(send: GeometryBatchSend, options?: GeometryBatcherOptions): GeometryBatcher;
|
|
41
43
|
//# sourceMappingURL=protocol-publisher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol-publisher.d.ts","sourceRoot":"","sources":["../src/protocol-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"protocol-publisher.d.ts","sourceRoot":"","sources":["../src/protocol-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAA;AAE5B,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,eAAe,CAAC,CAAA;AACrD,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,aAAa,CAAC,CAAA;AAExD,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IAC1C,4FAA4F;IAC5F,KAAK,IAAI,OAAO,CAAA;IAChB,gEAAgE;IAChE,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAcnC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAcnC;AAMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAyKjB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { watchAbort } from './abort.js';
|
|
1
2
|
import { GEOMETRY_PROTOCOL_VERSION, } from './protocol-types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Wraps placement updates in a versioned protocol message. Sequence numbers
|
|
@@ -46,8 +47,8 @@ export function createSizeMessagePublisher(address, send) {
|
|
|
46
47
|
const NOOP_SEND = () => false;
|
|
47
48
|
/**
|
|
48
49
|
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
49
|
-
* no authorization policy: callers must associate addresses with trusted
|
|
50
|
-
*
|
|
50
|
+
* no authorization policy: callers must associate addresses with trusted
|
|
51
|
+
* sources before accepting a delivered batch.
|
|
51
52
|
*/
|
|
52
53
|
export function createGeometryBatcher(send, options = {}) {
|
|
53
54
|
const anchors = new Map();
|
|
@@ -55,6 +56,7 @@ export function createGeometryBatcher(send, options = {}) {
|
|
|
55
56
|
let disposed = false;
|
|
56
57
|
let scheduled = false;
|
|
57
58
|
let flushing = false;
|
|
59
|
+
let removeAbortListener = () => { };
|
|
58
60
|
// `activeOptions` lets a caller keep reporting to the options object that
|
|
59
61
|
// was live when its flush() call started, even if a reentrant dispose()
|
|
60
62
|
// during send() has since cleared the instance-level `options` reference.
|
|
@@ -144,6 +146,26 @@ export function createGeometryBatcher(send, options = {}) {
|
|
|
144
146
|
flush();
|
|
145
147
|
});
|
|
146
148
|
}
|
|
149
|
+
const dispose = () => {
|
|
150
|
+
if (disposed)
|
|
151
|
+
return;
|
|
152
|
+
disposed = true;
|
|
153
|
+
removeAbortListener();
|
|
154
|
+
removeAbortListener = () => { };
|
|
155
|
+
anchors.clear();
|
|
156
|
+
pendingAnchors.clear();
|
|
157
|
+
// Callers may still mutate the original options object after
|
|
158
|
+
// construction (e.g. reassigning onError); flush() captures its own
|
|
159
|
+
// reference before send() runs, so an in-flight error report keeps
|
|
160
|
+
// reading that object even though dispose() drops the instance's
|
|
161
|
+
// long-lived reference here.
|
|
162
|
+
options = {};
|
|
163
|
+
send = NOOP_SEND;
|
|
164
|
+
};
|
|
165
|
+
if (options.signal?.aborted)
|
|
166
|
+
dispose();
|
|
167
|
+
else
|
|
168
|
+
removeAbortListener = watchAbort(options.signal, dispose);
|
|
147
169
|
return {
|
|
148
170
|
publish(message) {
|
|
149
171
|
if (disposed)
|
|
@@ -191,17 +213,6 @@ export function createGeometryBatcher(send, options = {}) {
|
|
|
191
213
|
pendingAnchors.delete(state);
|
|
192
214
|
anchors.delete(anchorId);
|
|
193
215
|
},
|
|
194
|
-
dispose
|
|
195
|
-
disposed = true;
|
|
196
|
-
anchors.clear();
|
|
197
|
-
pendingAnchors.clear();
|
|
198
|
-
// Callers may still mutate the original options object after
|
|
199
|
-
// construction (e.g. reassigning onError); flush() captures its own
|
|
200
|
-
// reference before send() runs, so an in-flight error report keeps
|
|
201
|
-
// reading that object even though dispose() drops the instance's
|
|
202
|
-
// long-lived reference here.
|
|
203
|
-
options = {};
|
|
204
|
-
send = NOOP_SEND;
|
|
205
|
-
},
|
|
216
|
+
dispose,
|
|
206
217
|
};
|
|
207
218
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAQnB;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CA8EtB"}
|