view-anchor 0.2.2 → 1.0.0-beta.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.
- package/README.md +71 -87
- package/README.zh-CN.md +59 -64
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/protocol-publisher.d.ts +7 -9
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +2 -8
- package/dist/protocol-types.d.ts +5 -8
- package/dist/protocol-types.d.ts.map +1 -1
- package/dist/protocol-types.js +3 -6
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +10 -2
- package/dist/react.d.ts +2 -15
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +43 -38
- package/dist/{size-advertiser.d.ts → size-anchor.d.ts} +6 -5
- package/dist/size-anchor.d.ts.map +1 -0
- package/dist/size-anchor.js +131 -0
- package/dist/types.d.ts +26 -32
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts +52 -33
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +186 -265
- package/docs/bidirectional-design.md +29 -34
- package/docs/index.html +656 -366
- package/docs/mechanism.md +35 -57
- package/docs/performance-report.md +12 -54
- package/docs/protocol.md +12 -3
- package/package.json +4 -6
- package/src/index.ts +5 -19
- package/src/protocol-publisher.ts +10 -16
- package/src/protocol-types.ts +5 -8
- package/src/protocol.ts +13 -7
- package/src/react.ts +51 -64
- package/src/size-anchor.ts +131 -0
- package/src/types.ts +29 -37
- package/src/view-anchor.ts +221 -287
- package/dist/measure-loop.d.ts +0 -24
- package/dist/measure-loop.d.ts.map +0 -1
- package/dist/measure-loop.js +0 -89
- package/dist/size-advertiser.d.ts.map +0 -1
- package/dist/size-advertiser.js +0 -94
- package/src/measure-loop.ts +0 -101
- package/src/size-advertiser.ts +0 -108
package/README.md
CHANGED
|
@@ -7,43 +7,41 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/view-anchor)
|
|
8
8
|
[](https://www.npmjs.com/package/view-anchor)
|
|
9
9
|
[](./LICENSE)
|
|
10
|
-
[](https://nodejs.org/)
|
|
11
10
|
|
|
12
11
|
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
13
12
|
|
|
14
|
-
> 🎮 **Live demo**:
|
|
13
|
+
> 🎮 **Live demo**: [3D interactive demo](https://lbb00.github.io/view-anchor/) runs the real core in your browser. Drag the splitters, scroll the page, and click its buttons directly in the tilted scene: surface A follows its placeholder, and surface B reports its content height back so the page resizes its placeholder.
|
|
15
14
|
|
|
16
15
|
## The problem
|
|
17
16
|
|
|
18
|
-
Some surfaces are positioned by application code rather than by CSS
|
|
17
|
+
Some surfaces are positioned by application code rather than by CSS — a canvas, a video overlay, an embedded document, or anything you can position from a rectangle. DOM layout moves placeholder elements, but cannot move those external surfaces.
|
|
19
18
|
|
|
20
|
-
Point `view-anchor` at a placeholder element. It measures
|
|
19
|
+
Point `view-anchor` at a placeholder element. It measures it on creation, on `ResizeObserver` notifications, and on window `resize`, then calls your `publish` function. Your function applies, stores, or sends that value however your application already does.
|
|
21
20
|
|
|
22
|
-
The core has no dependency on a host runtime, React, or a layout library
|
|
21
|
+
The core has no dependency on a host runtime, React, or a layout library, and uses only `ResizeObserver`, `requestAnimationFrame`, and `getBoundingClientRect`. React support lives in a separate `view-anchor/react` entry.
|
|
23
22
|
|
|
24
23
|
## What `publish` receives
|
|
25
24
|
|
|
26
|
-
`publish` is
|
|
25
|
+
`publish` is a synchronous function that the library calls with one of these plain values:
|
|
27
26
|
|
|
28
|
-
- `createViewAnchor` calls `publish(bounds)
|
|
29
|
-
- `
|
|
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.
|
|
27
|
+
- `createViewAnchor` calls `publish({ visible: true, bounds })` while shown, where `bounds` is `{ x, y, width, height }` in CSS pixels from `getBoundingClientRect()`, or `publish({ visible: false })` while hidden. The explicit `visible` flag distinguishes a visible-but-zero-sized element from one that is hidden or unmounted.
|
|
28
|
+
- `createSizeAnchor` calls `publish({ axis, extent })`, where `axis` is `block` (height) or `inline` (width), and `extent` is the target's rounded, non-negative border-box size on that axis in CSS pixels (padding and border included; content-box only where the browser does not report a border box).
|
|
31
29
|
|
|
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.
|
|
30
|
+
Return `false` only when the value was not accepted; a later measurement may retry it. Return `true` or nothing after accepting or queueing it. A hidden anchor measures nothing, so a rejected `{ visible: false }` is not retried on its own: call `update()` again to resend it. `useViewAnchor` does this for you on unmount.
|
|
33
31
|
|
|
34
|
-
##
|
|
32
|
+
## Performance
|
|
35
33
|
|
|
36
|
-
Geometry updates fire on every resize and, when following a drag, on every animation frame.
|
|
34
|
+
Geometry updates fire on every resize and, when following a drag, on every animation frame.
|
|
37
35
|
|
|
38
36
|
- **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
|
|
39
|
-
- **Dedupe
|
|
40
|
-
- **Frame following
|
|
37
|
+
- **Dedupe by default.** A `Placement` identical to the last accepted one is not passed to `publish` again; set `dedupe: false` to receive every measurement.
|
|
38
|
+
- **Frame following on demand.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then stops once the rectangle settles. Idle cost is zero; hidden or invalid targets are capped at 30 frames.
|
|
41
39
|
- **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
|
|
42
|
-
- **Latest-wins batching.** Messages queued in the same task are merged in a microtask
|
|
40
|
+
- **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.
|
|
43
41
|
- **Release on disposal.** Disposed handles stop observing and release their target and callback references, even when the caller keeps the handle.
|
|
44
|
-
- **
|
|
42
|
+
- **Tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
See the [performance report](./docs/performance-report.md) for a comparison with 0.2.2 and export sizes.
|
|
47
45
|
|
|
48
46
|
## Installation
|
|
49
47
|
|
|
@@ -53,7 +51,7 @@ pnpm add view-anchor
|
|
|
53
51
|
npm install view-anchor
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
React is an optional peer dependency
|
|
54
|
+
React is an optional peer dependency, needed only if you import from `view-anchor/react`. The root entry (`view-anchor`) and `view-anchor/protocol` have no React dependency and load without it installed.
|
|
57
55
|
|
|
58
56
|
## Usage
|
|
59
57
|
|
|
@@ -62,28 +60,33 @@ React is an optional peer dependency. Import the hooks from `view-anchor/react`.
|
|
|
62
60
|
```ts
|
|
63
61
|
import { createViewAnchor } from 'view-anchor'
|
|
64
62
|
|
|
65
|
-
const publish = (
|
|
66
|
-
applyBounds(bounds)
|
|
63
|
+
const publish = (placement) => {
|
|
64
|
+
if (placement.visible) applyBounds(placement.bounds)
|
|
65
|
+
else hideSurface()
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
const handle = createViewAnchor(target, {
|
|
70
|
-
|
|
69
|
+
visible: true,
|
|
71
70
|
publish,
|
|
71
|
+
followScroll: true, // re-measure when any ancestor scrolls
|
|
72
|
+
followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
|
|
73
|
+
treatZeroAreaAsHidden: true, // zero-area or display:none target → { visible: false }
|
|
74
|
+
holdSelector: '[role="separator"]', // default; pointerdown on a match keeps followGeometry open while held, null disables
|
|
75
|
+
dedupe: true, // default; set false to receive every measurement, even unchanged ones
|
|
72
76
|
})
|
|
73
77
|
|
|
74
|
-
handle.update({
|
|
78
|
+
handle.update({ visible: true, publish }) // apply new options and publish right away
|
|
79
|
+
handle.pulse() // re-measure after a move no observer sees (e.g. a class toggle moved the target without resizing it); closes once steady
|
|
75
80
|
handle.dispose() // stop observing; never publishes again
|
|
76
81
|
```
|
|
77
82
|
|
|
78
|
-
Set `
|
|
83
|
+
Set `visible: false` to collapse the surface. The core publishes `{ visible: false }` and stops observing. Your `publish` function decides whether that removes, hides, or retains the external surface.
|
|
79
84
|
|
|
80
|
-
|
|
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.
|
|
85
|
+
`createViewAnchor`, `createSizeAnchor`, and `createGeometryBatcher` accept `signal`. Aborting it is equivalent to `dispose()`; an already-aborted signal does not measure, publish, or install listeners.
|
|
83
86
|
|
|
84
87
|
```ts
|
|
85
88
|
const controller = new AbortController()
|
|
86
|
-
const handle = createViewAnchor(target, {
|
|
89
|
+
const handle = createViewAnchor(target, { visible: true, publish, signal: controller.signal })
|
|
87
90
|
|
|
88
91
|
controller.abort() // same cleanup as handle.dispose()
|
|
89
92
|
```
|
|
@@ -95,62 +98,44 @@ import { useViewAnchor } from 'view-anchor/react'
|
|
|
95
98
|
|
|
96
99
|
function DebugPanel({ visible }: { visible: boolean }) {
|
|
97
100
|
const ref = useViewAnchor({
|
|
98
|
-
|
|
99
|
-
publish:
|
|
101
|
+
visible,
|
|
102
|
+
publish: publishPanelPlacement,
|
|
103
|
+
followScroll: true,
|
|
104
|
+
followGeometry: true,
|
|
100
105
|
})
|
|
101
106
|
// The external surface follows this placeholder. Hiding or unmounting it
|
|
102
|
-
// sends
|
|
107
|
+
// sends { visible: false }, without deciding how the surface is stored.
|
|
103
108
|
return <div ref={ref} className="h-full w-full" />
|
|
104
109
|
}
|
|
105
110
|
```
|
|
106
111
|
|
|
107
|
-
The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames.
|
|
108
|
-
|
|
109
|
-
### Explicit visibility
|
|
110
|
-
|
|
111
|
-
A zero rectangle cannot tell a hidden view from one that is visible but currently 0×0. When that distinction matters, use the `Placement` API. It publishes `{ visible: true, bounds }` or `{ visible: false }` and adds opt-in scroll and geometry following:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
import { createPlacementAnchor } from 'view-anchor'
|
|
115
|
-
|
|
116
|
-
const handle = createPlacementAnchor(target, {
|
|
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
|
|
123
|
-
guardDisplayNone: true, // zero-area or display:none target → { visible: false }
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
handle.pulse() // open a short frame-following window, e.g. during a CSS transition
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
The React version is `usePlacementAnchor` from `view-anchor/react`.
|
|
112
|
+
The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames. It re-applies options on every update with the same reset semantics as `createViewAnchor` and `update()`: an omitted option resets to its default, not the previous value. An omitted `treatZeroAreaAsHidden`, `followScroll`, or `followGeometry` counts as `false`; an omitted `holdSelector` resets to `[role="separator"]`; an omitted `dedupe` resets to `true`. Pass `null` / `false` to turn `holdSelector` / `dedupe` off.
|
|
130
113
|
|
|
131
114
|
### Let content drive the size
|
|
132
115
|
|
|
133
|
-
Sometimes the hosted surface's size should come from its own content, for example a toolbar rendered by downstream code. Run `
|
|
116
|
+
Sometimes the hosted surface's size should come from its own content, for example a toolbar rendered by downstream code. Run `createSizeAnchor` inside the hosted document. It reports the content size back so a DOM placeholder in the host can grow to match:
|
|
134
117
|
|
|
135
118
|
```ts
|
|
136
|
-
import {
|
|
119
|
+
import { createSizeAnchor } from 'view-anchor'
|
|
120
|
+
|
|
121
|
+
const publish = (size) => {
|
|
122
|
+
updatePlaceholderSize(size)
|
|
123
|
+
}
|
|
137
124
|
|
|
138
|
-
const handle =
|
|
139
|
-
axis: 'block', // one axis per
|
|
140
|
-
publish
|
|
141
|
-
updatePlaceholderSize(size)
|
|
142
|
-
},
|
|
125
|
+
const handle = createSizeAnchor(contentWrapper, {
|
|
126
|
+
axis: 'block', // one axis per size anchor: block = height, inline = width
|
|
127
|
+
publish,
|
|
143
128
|
})
|
|
144
129
|
|
|
145
|
-
handle.update(publish) //
|
|
130
|
+
handle.update({ publish }) // apply new options and report the current size again; an omitted dedupe resets to true
|
|
146
131
|
handle.dispose() // stop observing; never reports again
|
|
147
132
|
```
|
|
148
133
|
|
|
149
|
-
>
|
|
134
|
+
> 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).
|
|
150
135
|
|
|
151
136
|
### Versioned transport across a boundary
|
|
152
137
|
|
|
153
|
-
The core hands you plain `Bounds`, `Placement`, and `
|
|
138
|
+
The core hands you plain `Bounds`, `Placement`, and `SizeMeasurement` 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:
|
|
154
139
|
|
|
155
140
|
```ts
|
|
156
141
|
import {
|
|
@@ -173,45 +158,44 @@ if (decoded.ok) {
|
|
|
173
158
|
}
|
|
174
159
|
```
|
|
175
160
|
|
|
176
|
-
Two rules keep the ordering correct:
|
|
177
|
-
|
|
178
161
|
- **Keep one publisher per `{ anchorId, generation }`.** The batcher and `createGeometrySequenceGuard` remember the highest sequence number for each message kind per anchor. A publisher rebuilt for the same address restarts at sequence 1 and its messages are dropped as stale. In React, hold it in `useMemo` or `useRef`. Bump `generation` when you really want a fresh start.
|
|
179
|
-
- **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger. A batching publisher returns `true` once queued and owns any later retries.
|
|
162
|
+
- **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger (a rejected `{ visible: false }` waits for the next `update()`). A batching publisher returns `true` once queued and owns any later retries.
|
|
180
163
|
|
|
181
164
|
The full contract is in [docs/protocol.md](./docs/protocol.md).
|
|
182
165
|
|
|
183
166
|
## API
|
|
184
167
|
|
|
185
|
-
| Export
|
|
186
|
-
|
|
|
187
|
-
| `createViewAnchor(target, opts)`
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
168
|
+
| Export | Kind | Purpose |
|
|
169
|
+
| ----------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
170
|
+
| `createViewAnchor(target, opts)` | function | Publish a `Placement`, with opt-in `followScroll` / `followGeometry` / `treatZeroAreaAsHidden` / `holdSelector` / `dedupe`, and `pulse()`. |
|
|
171
|
+
| `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
|
|
172
|
+
| `createSizeAnchor(target, opts)` | function | Call `publish({ axis, extent })` with one content-size axis. |
|
|
173
|
+
| `useViewAnchor(opts)` from `view-anchor/react` | hook | React adapter for `createViewAnchor`, returning a ref callback for a placeholder element. |
|
|
174
|
+
| `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
|
|
175
|
+
| `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
|
|
176
|
+
| `Publisher<T>` / `PublishResult` | type | Signature of the `publish` callback and its return value (`void \| boolean`). |
|
|
177
|
+
| `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
|
|
178
|
+
| `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
|
|
179
|
+
| `SizeAxis` / `SizeMeasurement` | type | Axis and payload types for the reverse direction. |
|
|
180
|
+
| `SizeAnchorOptions` / `SizeAnchorHandle` | type | Options and handle for `createSizeAnchor`. |
|
|
181
|
+
| `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
|
|
182
|
+
|
|
183
|
+
## Versioning
|
|
184
|
+
|
|
185
|
+
Within 1.x, `view-anchor` does not remove or rename any public export from `.`, `view-anchor/react`, or `view-anchor/protocol`, and does not change the default behavior for input that is already valid. Minor releases add functionality; patch releases fix bugs. An interface slated for removal is deprecated first and only removed in 2.0.
|
|
202
186
|
|
|
203
187
|
## Documentation
|
|
204
188
|
|
|
205
|
-
- [docs/mechanism.md](./docs/mechanism.md): how the forward direction works. Synchronous publishing,
|
|
189
|
+
- [docs/mechanism.md](./docs/mechanism.md): how the forward direction works. Synchronous publishing, default dedup and `dedupe: false`, the `Placement` / visibility / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
|
|
206
190
|
- [docs/bidirectional-design.md](./docs/bidirectional-design.md): running both directions at once. Why the forward path is synchronous while the reverse path uses animation frames, single-axis ownership, and where the trust boundary sits.
|
|
207
191
|
- [docs/protocol.md](./docs/protocol.md): message envelopes, validation, ordering, batching, and what happens on failure.
|
|
208
|
-
- [docs/performance-report.md](./docs/performance-report.md):
|
|
192
|
+
- [docs/performance-report.md](./docs/performance-report.md): performance summary and export sizes.
|
|
209
193
|
|
|
210
194
|
## Contributing
|
|
211
195
|
|
|
212
|
-
|
|
196
|
+
Before submitting, run `pnpm lint`, `pnpm format:check`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` prints the data used to update the performance report.
|
|
213
197
|
|
|
214
|
-
Changes that affect a published version must include a Changeset. Run `pnpm changeset`, select the version bump, and describe the user-visible change. Merging
|
|
198
|
+
Changes that affect a published version must include a Changeset. Run `pnpm changeset`, select the version bump, and describe the user-visible change. Merging into `main` opens a version PR; merging that PR publishes the package.
|
|
215
199
|
|
|
216
200
|
## License
|
|
217
201
|
|
package/README.zh-CN.md
CHANGED
|
@@ -7,27 +7,25 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/view-anchor)
|
|
8
8
|
[](https://www.npmjs.com/package/view-anchor)
|
|
9
9
|
[](./LICENSE)
|
|
10
|
-
[](https://nodejs.org/)
|
|
11
10
|
|
|
12
11
|
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
13
12
|
|
|
14
|
-
> [在线演示](https://lbb00.github.io/view-anchor/)
|
|
13
|
+
> [在线演示](https://lbb00.github.io/view-anchor/):直接在倾斜的 3D 场景里拖动分隔条、滚动页面、点击按钮。画面 A 跟随占位元素;画面 B 把自己的内容高度报回页面,页面据此调整它的占位。
|
|
15
14
|
|
|
16
15
|
## 它解决什么问题
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
有些画面的位置由应用自己摆放(叠层、嵌入式文档或能根据矩形定位的渲染表面)。DOM 布局会移动占位元素,但不会自动更新这些画面的位置。
|
|
19
18
|
|
|
20
|
-
`view-anchor`
|
|
19
|
+
`view-anchor` 负责测量占位元素,在尺寸变化或窗口大小变化时同步调用你提供的 `publish` 函数。函数如何使用这个值由应用决定。
|
|
21
20
|
|
|
22
21
|
## `publish` 收到什么
|
|
23
22
|
|
|
24
|
-
`publish`
|
|
23
|
+
`publish` 是一个同步回调,不同接口会传入不同的普通对象:
|
|
25
24
|
|
|
26
|
-
- `createViewAnchor`
|
|
27
|
-
- `
|
|
28
|
-
- `createSizeAdvertiser` 调用 `publish({ axis, extent })`。`axis` 为 `block`(高度)或 `inline`(宽度),`extent` 是取整后且不小于零的内容尺寸。
|
|
25
|
+
- `createViewAnchor` 在显示时调用 `publish({ visible: true, bounds })`(`bounds` 是相对视口、取整后的 CSS 像素矩形 `{ x, y, width, height }`),隐藏时调用 `publish({ visible: false })`。显式的 `visible` 用于区分“可见但恰好是 0×0”和“已隐藏或未挂载”。
|
|
26
|
+
- `createSizeAnchor` 调用 `publish({ axis, extent })`。`axis` 为 `block`(高度)或 `inline`(宽度),`extent` 是目标元素在该轴上的 border-box 尺寸(包含 padding 和 border;浏览器不提供 border-box 时退回 content-box),单位 CSS 像素,取整且不小于零。
|
|
29
27
|
|
|
30
|
-
数据已接收或已经入队时,返回 `true` 或不返回值;当前不能接收时才返回 `false
|
|
28
|
+
数据已接收或已经入队时,返回 `true` 或不返回值;当前不能接收时才返回 `false`,下一次测量会重试被拒绝的值。隐藏状态下不再测量,所以被拒绝的 `{ visible: false }` 不会自动重发,需要再调用一次 `update()`;`useViewAnchor` 会在卸载时替你补发。
|
|
31
29
|
|
|
32
30
|
## 安装
|
|
33
31
|
|
|
@@ -37,7 +35,7 @@ pnpm add view-anchor
|
|
|
37
35
|
npm install view-anchor
|
|
38
36
|
```
|
|
39
37
|
|
|
40
|
-
React 是可选 peer dependency
|
|
38
|
+
React 是可选 peer dependency,只有从 `view-anchor/react` 导入时才需要。根入口(`view-anchor`)和 `view-anchor/protocol` 不依赖 React,未安装 React 也能加载。
|
|
41
39
|
|
|
42
40
|
## 使用
|
|
43
41
|
|
|
@@ -46,29 +44,34 @@ React 是可选 peer dependency。Hook 从 `view-anchor/react` 导入;根入
|
|
|
46
44
|
```ts
|
|
47
45
|
import { createViewAnchor } from 'view-anchor'
|
|
48
46
|
|
|
49
|
-
const publish = (
|
|
50
|
-
applyBoundsToExternalSurface(bounds)
|
|
47
|
+
const publish = (placement) => {
|
|
48
|
+
if (placement.visible) applyBoundsToExternalSurface(placement.bounds)
|
|
49
|
+
else hideExternalSurface()
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
const handle = createViewAnchor(placeholderEl, {
|
|
54
|
-
|
|
53
|
+
visible: true,
|
|
55
54
|
publish,
|
|
55
|
+
followScroll: true, // 祖先容器滚动时重新测量
|
|
56
|
+
followGeometry: true, // 拖拽或滚动期间逐帧测量;稳定后停止
|
|
57
|
+
treatZeroAreaAsHidden: true, // 零面积或 display:none 时发布 { visible: false }
|
|
58
|
+
holdSelector: '[role="separator"]', // 默认值;命中该选择器的 pointerdown 按住期间保持 followGeometry 开启,传 null 关闭
|
|
59
|
+
dedupe: true, // 默认值;传 false 让每次测量都发布,即使值没变
|
|
56
60
|
})
|
|
57
61
|
|
|
58
|
-
handle.update({
|
|
62
|
+
handle.update({ visible: true, publish }) // 更新选项并立即重新发布
|
|
63
|
+
handle.pulse() // 位置变了但没有任何观察器能感知时(如一次类名切换让目标移动了但尺寸没变)主动重测;稳定后自动停止
|
|
59
64
|
handle.dispose() // 停止观察;之后不会再发布
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
把 `visible` 设为 `false` 会发布 `{ visible: false }` 并停止观察。你的 `publish` 函数可以把它解释为隐藏、移除,或保留外部画面。
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`createViewAnchor`、`createPlacementAnchor`、`createSizeAdvertiser` 和 `createGeometryBatcher` 都支持 `signal`。调用 `AbortController.abort()` 的清理效果等同于 `dispose()`;创建前已经 abort 的 signal 不会开始测量或安装监听。
|
|
69
|
+
`createViewAnchor`、`createSizeAnchor` 和 `createGeometryBatcher` 都支持 `signal`。调用 `AbortController.abort()` 的清理效果等同于 `dispose()`;创建前已经 abort 的 signal 不会开始测量或安装监听。
|
|
67
70
|
|
|
68
71
|
```ts
|
|
69
72
|
const controller = new AbortController()
|
|
70
73
|
const handle = createViewAnchor(placeholderEl, {
|
|
71
|
-
|
|
74
|
+
visible: true,
|
|
72
75
|
publish,
|
|
73
76
|
signal: controller.signal,
|
|
74
77
|
})
|
|
@@ -83,49 +86,35 @@ import { useViewAnchor } from 'view-anchor/react'
|
|
|
83
86
|
|
|
84
87
|
function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
|
|
85
88
|
const ref = useViewAnchor({
|
|
86
|
-
|
|
87
|
-
publish:
|
|
89
|
+
visible,
|
|
90
|
+
publish: applyPlacementToExternalSurface,
|
|
91
|
+
followScroll: true,
|
|
92
|
+
followGeometry: true,
|
|
88
93
|
})
|
|
89
94
|
|
|
90
95
|
return <div ref={ref} className="h-full w-full" />
|
|
91
96
|
}
|
|
92
97
|
```
|
|
93
98
|
|
|
94
|
-
元素卸载时,Hook
|
|
95
|
-
|
|
96
|
-
### 显式可见性
|
|
97
|
-
|
|
98
|
-
全零矩形无法区分“已隐藏”和“可见但恰好是 0×0”。需要这个区分时使用 `Placement` API:
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
import { createPlacementAnchor } from 'view-anchor'
|
|
102
|
-
|
|
103
|
-
const handle = createPlacementAnchor(target, {
|
|
104
|
-
visible: true,
|
|
105
|
-
publish: applyPlacement,
|
|
106
|
-
followScroll: true, // 祖先容器滚动时重新测量
|
|
107
|
-
followGeometry: true, // 拖拽或滚动期间逐帧测量;稳定后停止
|
|
108
|
-
guardDisplayNone: true, // 零面积或 display:none 时发布 { visible: false }
|
|
109
|
-
})
|
|
99
|
+
元素卸载时,Hook 会先发布 `{ visible: false }`,再释放监听。它兼容 React 18 和 19 的 StrictMode 重挂载,不会因此发布过期帧。
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
React 版本为 `view-anchor/react` 导出的 `usePlacementAnchor`。
|
|
101
|
+
它每次调用都会重新应用选项,规则与 `createViewAnchor` 和 `update()` 一致:省略任何一项都会重置为默认值,而不是沿用上次的值。省略 `treatZeroAreaAsHidden`、`followScroll` 或 `followGeometry` 会重置为 `false`;省略 `holdSelector` 会重置为 `[role="separator"]`;省略 `dedupe` 会重置为 `true`。要关闭 `holdSelector` / `dedupe`,传 `null` / `false`。
|
|
115
102
|
|
|
116
103
|
### 由内容决定占位尺寸
|
|
117
104
|
|
|
118
|
-
当外部画面的内容尺寸需要反过来调整占位元素时,在内容所在的文档中使用 `
|
|
105
|
+
当外部画面的内容尺寸需要反过来调整占位元素时,在内容所在的文档中使用 `createSizeAnchor`:
|
|
119
106
|
|
|
120
107
|
```ts
|
|
121
|
-
import {
|
|
108
|
+
import { createSizeAnchor } from 'view-anchor'
|
|
122
109
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
110
|
+
const publish = updatePlaceholderSize
|
|
111
|
+
|
|
112
|
+
const handle = createSizeAnchor(contentWrapper, {
|
|
113
|
+
axis: 'block', // 一个 size anchor 只负责一个轴:block = 高度,inline = 宽度
|
|
114
|
+
publish,
|
|
126
115
|
})
|
|
127
116
|
|
|
128
|
-
handle.update(
|
|
117
|
+
handle.update({ publish }) // 应用新选项并立即重新报告当前尺寸;省略 dedupe 会重置为 true
|
|
129
118
|
handle.dispose()
|
|
130
119
|
```
|
|
131
120
|
|
|
@@ -133,7 +122,7 @@ handle.dispose()
|
|
|
133
122
|
|
|
134
123
|
### 需要传递、校验和排序时
|
|
135
124
|
|
|
136
|
-
核心只交付 `Bounds`、`Placement` 和 `
|
|
125
|
+
核心只交付 `Bounds`、`Placement` 和 `SizeMeasurement`。如果应用需要把这些值交给别的页面、环境或异步通道,可选的 `view-anchor/protocol` 提供消息版本、输入校验、乱序过滤和微任务合并。
|
|
137
126
|
|
|
138
127
|
```ts
|
|
139
128
|
import {
|
|
@@ -161,34 +150,40 @@ if (decoded.ok) {
|
|
|
161
150
|
|
|
162
151
|
## 性能边界
|
|
163
152
|
|
|
164
|
-
-
|
|
153
|
+
- 默认去重:与上一次已发布的 `Placement`/尺寸完全相同不会再次调用 `publish`;传 `dedupe: false` 可以让每次测量都发布。
|
|
165
154
|
- `followGeometry` 只在需要时启动动画帧轮询,稳定后停止。
|
|
166
155
|
- `createGeometryBatcher` 会合并同一微任务内的更新;每个锚点的位置和尺寸各保留最新值。
|
|
167
156
|
- `dispose()` 或 abort 会停止监听并释放长期持有的元素和回调引用。
|
|
168
157
|
|
|
169
|
-
|
|
158
|
+
与 0.2.2 的对比和导出体积见 [性能报告](./docs/performance-report.md)。
|
|
170
159
|
|
|
171
160
|
## API
|
|
172
161
|
|
|
173
|
-
| 导出
|
|
174
|
-
|
|
|
175
|
-
| `createViewAnchor(target, opts)`
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
162
|
+
| 导出 | 类型 | 用途 |
|
|
163
|
+
| ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
164
|
+
| `createViewAnchor(target, opts)` | 函数 | 发布带可见性的 `Placement`,可选 `followScroll`、`followGeometry`、`treatZeroAreaAsHidden`、`holdSelector`、`dedupe`,含 `pulse()`。 |
|
|
165
|
+
| `measurePlacement(target)` | 函数 | 读取当前矩形,返回 `{ visible: true, bounds }`。 |
|
|
166
|
+
| `createSizeAnchor(target, opts)` | 函数 | 用 `publish({ axis, extent })` 上报一个内容尺寸轴。 |
|
|
167
|
+
| `useViewAnchor(opts)` | Hook | `createViewAnchor` 的 React 适配,返回占位元素的 ref 回调。 |
|
|
168
|
+
| `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
|
|
169
|
+
| `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
|
|
170
|
+
| `Publisher<T>` / `PublishResult` | 类型 | `publish` 回调的签名及其返回值(`void \| boolean`)。 |
|
|
171
|
+
| `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项与句柄。 |
|
|
172
|
+
| `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | `useViewAnchor` 的选项与 ref 类型。 |
|
|
173
|
+
| `SizeAxis` / `SizeMeasurement` | 类型 | 内容尺寸上报的轴和数据。 |
|
|
174
|
+
| `SizeAnchorOptions` / `SizeAnchorHandle` | 类型 | `createSizeAnchor` 的选项与句柄。 |
|
|
175
|
+
| `view-anchor/protocol` | 函数 + 类型 | 消息封装、校验、排序和批处理。 |
|
|
176
|
+
|
|
177
|
+
## 版本承诺
|
|
178
|
+
|
|
179
|
+
1.x 版本内,`view-anchor` 不会删除或重命名 `.`、`view-anchor/react`、`view-anchor/protocol` 的任何公开导出,也不会改变已有合法输入的默认行为。minor 版本只新增功能,patch 版本只修 bug。要移除的接口会先标记为废弃,等到 2.0 才真正删除。
|
|
185
180
|
|
|
186
181
|
## 文档
|
|
187
182
|
|
|
188
|
-
- [机制说明](./docs/mechanism.md)
|
|
183
|
+
- [机制说明](./docs/mechanism.md):正向测量的触发条件、默认去重与 `dedupe: false`、可见性与 React 生命周期。
|
|
189
184
|
- [双向几何设计](./docs/bidirectional-design.md):内容尺寸反向调整占位元素时的单轴约束。
|
|
190
185
|
- [通信协议](./docs/protocol.md):消息格式、校验、排序和批处理。
|
|
191
|
-
- [性能报告](./docs/performance-report.md)
|
|
186
|
+
- [性能报告](./docs/performance-report.md):性能结论和导出体积。
|
|
192
187
|
|
|
193
188
|
## 参与贡献
|
|
194
189
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* view-anchor: keeps an external surface aligned with a DOM element's geometry.
|
|
3
3
|
*/
|
|
4
|
-
export { createViewAnchor, measurePlacement
|
|
5
|
-
export type {
|
|
6
|
-
export type { Bounds, Placement, Publisher, PublishResult
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export type { AdvertisedAxis, AdvertisedSize, SizeAdvertiserOptions, SizeAdvertiserHandle, } from './types.js';
|
|
10
|
-
export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js';
|
|
4
|
+
export { createViewAnchor, measurePlacement } from './view-anchor.js';
|
|
5
|
+
export type { ViewAnchorOptions, ViewAnchorHandle } from './view-anchor.js';
|
|
6
|
+
export type { Bounds, Placement, Publisher, PublishResult } from './types.js';
|
|
7
|
+
export { createSizeAnchor } from './size-anchor.js';
|
|
8
|
+
export type { SizeAxis, SizeMeasurement, SizeAnchorOptions, SizeAnchorHandle } from './types.js';
|
|
11
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACrE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAC3E,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* view-anchor: keeps an external surface aligned with a DOM element's geometry.
|
|
3
3
|
*/
|
|
4
|
-
export { createViewAnchor, measurePlacement
|
|
5
|
-
export {
|
|
6
|
-
export { useViewAnchor } from './react.js';
|
|
4
|
+
export { createViewAnchor, measurePlacement } from './view-anchor.js';
|
|
5
|
+
export { createSizeAnchor } from './size-anchor.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SizeMeasurement, Placement, Publisher } from './types.js';
|
|
2
2
|
import { type GeometryAddress, type GeometryBatch, type GeometryMessage } from './protocol-types.js';
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
3
|
+
export type GeometryMessageSender = Publisher<GeometryMessage>;
|
|
4
|
+
export type GeometryBatchSender = 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;
|
|
@@ -27,17 +27,15 @@ export interface GeometryBatcher {
|
|
|
27
27
|
* recreating a publisher for the same address causes its messages to be dropped.
|
|
28
28
|
* Increment `generation` when intentionally resetting the publisher.
|
|
29
29
|
*/
|
|
30
|
-
export declare function createPlacementMessagePublisher(address: GeometryAddress, send:
|
|
30
|
+
export declare function createPlacementMessagePublisher(address: GeometryAddress, send: GeometryMessageSender): (placement: Placement) => boolean;
|
|
31
31
|
/**
|
|
32
32
|
* Wraps size updates in a versioned protocol message.
|
|
33
33
|
* Follows the same stability rule: keep one publisher per `{ anchorId, generation }`,
|
|
34
34
|
* and increment `generation` when resetting.
|
|
35
35
|
*/
|
|
36
|
-
export declare function createSizeMessagePublisher(address: GeometryAddress, send:
|
|
36
|
+
export declare function createSizeMessagePublisher(address: GeometryAddress, send: GeometryMessageSender): (size: SizeMeasurement) => boolean;
|
|
37
37
|
/**
|
|
38
|
-
* Coalesces same-task messages without adding a rendering-frame delay.
|
|
39
|
-
* no authorization policy: callers must associate addresses with trusted
|
|
40
|
-
* sources before accepting a delivered batch.
|
|
38
|
+
* Coalesces same-task messages without adding a rendering-frame delay.
|
|
41
39
|
*/
|
|
42
|
-
export declare function createGeometryBatcher(send:
|
|
40
|
+
export declare function createGeometryBatcher(send: GeometryBatchSender, options?: GeometryBatcherOptions): GeometryBatcher;
|
|
43
41
|
//# 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,
|
|
1
|
+
{"version":3,"file":"protocol-publisher.d.ts","sourceRoot":"","sources":["../src/protocol-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEvE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAA;AAE5B,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAAC,eAAe,CAAC,CAAA;AAC9D,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,aAAa,CAAC,CAAA;AAE1D,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,qBAAqB,GAC1B,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAcnC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,qBAAqB,GAC1B,CAAC,IAAI,EAAE,eAAe,KAAK,OAAO,CAcpC;AAMD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,EACzB,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAqKjB"}
|
|
@@ -46,9 +46,7 @@ export function createSizeMessagePublisher(address, send) {
|
|
|
46
46
|
// keep the caller's transport closure (and whatever it captured) alive.
|
|
47
47
|
const NOOP_SEND = () => false;
|
|
48
48
|
/**
|
|
49
|
-
* Coalesces same-task messages without adding a rendering-frame delay.
|
|
50
|
-
* no authorization policy: callers must associate addresses with trusted
|
|
51
|
-
* sources before accepting a delivered batch.
|
|
49
|
+
* Coalesces same-task messages without adding a rendering-frame delay.
|
|
52
50
|
*/
|
|
53
51
|
export function createGeometryBatcher(send, options = {}) {
|
|
54
52
|
const anchors = new Map();
|
|
@@ -154,11 +152,7 @@ export function createGeometryBatcher(send, options = {}) {
|
|
|
154
152
|
removeAbortListener = () => { };
|
|
155
153
|
anchors.clear();
|
|
156
154
|
pendingAnchors.clear();
|
|
157
|
-
//
|
|
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.
|
|
155
|
+
// Drop the long-lived reference; flush() captures its own before send() runs.
|
|
162
156
|
options = {};
|
|
163
157
|
send = NOOP_SEND;
|
|
164
158
|
};
|