view-anchor 0.2.2-alpha.0 → 1.0.0-beta.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 +93 -79
- package/README.zh-CN.md +100 -92
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +9 -0
- 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 +9 -9
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +20 -15
- 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 +27 -29
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts +53 -32
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +205 -265
- package/docs/bidirectional-design.md +39 -55
- package/docs/index.html +682 -366
- package/docs/mechanism.md +41 -63
- package/docs/performance-report.md +12 -54
- package/docs/protocol.md +19 -10
- package/package.json +6 -16
- package/src/abort.ts +9 -0
- package/src/index.ts +5 -19
- package/src/protocol-publisher.ts +29 -23
- 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 +30 -34
- package/src/view-anchor.ts +241 -284
- 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 -84
- package/src/measure-loop.ts +0 -101
- package/src/size-advertiser.ts +0 -97
package/README.md
CHANGED
|
@@ -2,38 +2,46 @@
|
|
|
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)
|
|
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
|
|
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
|
-
`view-anchor`
|
|
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
|
|
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
|
-
|
|
25
|
+
`publish` is a synchronous function that the library calls with one of these plain values:
|
|
26
|
+
|
|
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).
|
|
29
|
+
|
|
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.
|
|
31
|
+
|
|
32
|
+
## Performance
|
|
33
|
+
|
|
34
|
+
Geometry updates fire on every resize and, when following a drag, on every animation frame.
|
|
27
35
|
|
|
28
36
|
- **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
|
|
29
|
-
- **Dedupe
|
|
30
|
-
- **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.
|
|
31
39
|
- **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
|
|
32
|
-
- **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.
|
|
33
41
|
- **Release on disposal.** Disposed handles stop observing and release their target and callback references, even when the caller keeps the handle.
|
|
34
|
-
- **
|
|
42
|
+
- **Tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
See the [performance report](./docs/performance-report.md) for a comparison with 0.2.2 and export sizes.
|
|
37
45
|
|
|
38
46
|
## Installation
|
|
39
47
|
|
|
@@ -43,7 +51,7 @@ pnpm add view-anchor
|
|
|
43
51
|
npm install view-anchor
|
|
44
52
|
```
|
|
45
53
|
|
|
46
|
-
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.
|
|
47
55
|
|
|
48
56
|
## Usage
|
|
49
57
|
|
|
@@ -52,16 +60,36 @@ React is an optional peer dependency. Import the hooks from `view-anchor/react`.
|
|
|
52
60
|
```ts
|
|
53
61
|
import { createViewAnchor } from 'view-anchor'
|
|
54
62
|
|
|
63
|
+
const publish = (placement) => {
|
|
64
|
+
if (placement.visible) applyBounds(placement.bounds)
|
|
65
|
+
else hideSurface()
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
const handle = createViewAnchor(target, {
|
|
56
|
-
|
|
57
|
-
publish
|
|
69
|
+
visible: true,
|
|
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
|
|
58
76
|
})
|
|
59
77
|
|
|
60
|
-
handle.update({
|
|
61
|
-
handle.
|
|
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
|
|
80
|
+
handle.dispose() // stop observing; never publishes again
|
|
62
81
|
```
|
|
63
82
|
|
|
64
|
-
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.
|
|
84
|
+
|
|
85
|
+
`createViewAnchor`, `createSizeAnchor`, and `createGeometryBatcher` accept `signal`. Aborting it is equivalent to `dispose()`; an already-aborted signal does not measure, publish, or install listeners.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const controller = new AbortController()
|
|
89
|
+
const handle = createViewAnchor(target, { visible: true, publish, signal: controller.signal })
|
|
90
|
+
|
|
91
|
+
controller.abort() // same cleanup as handle.dispose()
|
|
92
|
+
```
|
|
65
93
|
|
|
66
94
|
### React
|
|
67
95
|
|
|
@@ -70,57 +98,44 @@ import { useViewAnchor } from 'view-anchor/react'
|
|
|
70
98
|
|
|
71
99
|
function DebugPanel({ visible }: { visible: boolean }) {
|
|
72
100
|
const ref = useViewAnchor({
|
|
73
|
-
|
|
74
|
-
publish:
|
|
101
|
+
visible,
|
|
102
|
+
publish: publishPanelPlacement,
|
|
103
|
+
followScroll: true,
|
|
104
|
+
followGeometry: true,
|
|
75
105
|
})
|
|
76
|
-
// The
|
|
77
|
-
//
|
|
106
|
+
// The external surface follows this placeholder. Hiding or unmounting it
|
|
107
|
+
// sends { visible: false }, without deciding how the surface is stored.
|
|
78
108
|
return <div ref={ref} className="h-full w-full" />
|
|
79
109
|
}
|
|
80
110
|
```
|
|
81
111
|
|
|
82
|
-
The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames.
|
|
83
|
-
|
|
84
|
-
### Explicit visibility
|
|
85
|
-
|
|
86
|
-
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:
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
import { createPlacementAnchor } from 'view-anchor'
|
|
90
|
-
|
|
91
|
-
const handle = createPlacementAnchor(target, {
|
|
92
|
-
publish: (placement) => { ... },
|
|
93
|
-
followScroll: true, // re-measure when any ancestor scrolls
|
|
94
|
-
followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
|
|
95
|
-
guardDisplayNone: true, // zero-area or display:none target → { visible: false }
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
handle.pulse() // open a short frame-following window, e.g. during a CSS transition
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
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.
|
|
102
113
|
|
|
103
114
|
### Let content drive the size
|
|
104
115
|
|
|
105
|
-
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:
|
|
106
117
|
|
|
107
118
|
```ts
|
|
108
|
-
import {
|
|
119
|
+
import { createSizeAnchor } from 'view-anchor'
|
|
109
120
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
const publish = (size) => {
|
|
122
|
+
updatePlaceholderSize(size)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handle = createSizeAnchor(contentWrapper, {
|
|
126
|
+
axis: 'block', // one axis per size anchor: block = height, inline = width
|
|
127
|
+
publish,
|
|
113
128
|
})
|
|
114
129
|
|
|
115
|
-
handle.update(publish) //
|
|
116
|
-
handle.dispose()
|
|
130
|
+
handle.update({ publish }) // apply new options and report the current size again; an omitted dedupe resets to true
|
|
131
|
+
handle.dispose() // stop observing; never reports again
|
|
117
132
|
```
|
|
118
133
|
|
|
119
|
-
>
|
|
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).
|
|
120
135
|
|
|
121
136
|
### Versioned transport across a boundary
|
|
122
137
|
|
|
123
|
-
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:
|
|
124
139
|
|
|
125
140
|
```ts
|
|
126
141
|
import {
|
|
@@ -129,59 +144,58 @@ import {
|
|
|
129
144
|
decodeGeometryWireValue,
|
|
130
145
|
} from 'view-anchor/protocol'
|
|
131
146
|
|
|
132
|
-
//
|
|
133
|
-
const batcher = createGeometryBatcher(
|
|
147
|
+
// `sendGeometryBatch` is supplied by your application.
|
|
148
|
+
const batcher = createGeometryBatcher(sendGeometryBatch)
|
|
134
149
|
const publish = createPlacementMessagePublisher(
|
|
135
150
|
{ anchorId: 'editor', generation: 3 },
|
|
136
151
|
batcher.publish,
|
|
137
152
|
)
|
|
138
153
|
|
|
139
|
-
// receiving side
|
|
154
|
+
// receiving side
|
|
140
155
|
const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
|
|
141
156
|
if (decoded.ok) {
|
|
142
157
|
/* check the sender, then apply only newer messages */
|
|
143
158
|
}
|
|
144
159
|
```
|
|
145
160
|
|
|
146
|
-
Two rules keep the ordering correct:
|
|
147
|
-
|
|
148
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.
|
|
149
|
-
- **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.
|
|
150
163
|
|
|
151
164
|
The full contract is in [docs/protocol.md](./docs/protocol.md).
|
|
152
165
|
|
|
153
166
|
## API
|
|
154
167
|
|
|
155
|
-
| Export
|
|
156
|
-
|
|
|
157
|
-
| `createViewAnchor(target, opts)`
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
172
186
|
|
|
173
187
|
## Documentation
|
|
174
188
|
|
|
175
|
-
- [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).
|
|
176
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.
|
|
177
191
|
- [docs/protocol.md](./docs/protocol.md): message envelopes, validation, ordering, batching, and what happens on failure.
|
|
178
|
-
- [docs/performance-report.md](./docs/performance-report.md):
|
|
192
|
+
- [docs/performance-report.md](./docs/performance-report.md): performance summary and export sizes.
|
|
179
193
|
|
|
180
194
|
## Contributing
|
|
181
195
|
|
|
182
|
-
|
|
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.
|
|
183
197
|
|
|
184
|
-
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.
|
|
185
199
|
|
|
186
200
|
## License
|
|
187
201
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,39 +1,31 @@
|
|
|
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)
|
|
9
9
|
[](./LICENSE)
|
|
10
|
-
[](https://nodejs.org/)
|
|
11
10
|
|
|
12
11
|
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
13
12
|
|
|
14
|
-
>
|
|
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
|
-
|
|
23
|
+
`publish` 是一个同步回调,不同接口会传入不同的普通对象:
|
|
25
24
|
|
|
26
|
-
|
|
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 像素,取整且不小于零。
|
|
27
27
|
|
|
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 或你的应用负载。
|
|
28
|
+
数据已接收或已经入队时,返回 `true` 或不返回值;当前不能接收时才返回 `false`,下一次测量会重试被拒绝的值。隐藏状态下不再测量,所以被拒绝的 `{ visible: false }` 不会自动重发,需要再调用一次 `update()`;`useViewAnchor` 会在卸载时替你补发。
|
|
37
29
|
|
|
38
30
|
## 安装
|
|
39
31
|
|
|
@@ -43,84 +35,94 @@ pnpm add view-anchor
|
|
|
43
35
|
npm install view-anchor
|
|
44
36
|
```
|
|
45
37
|
|
|
46
|
-
React
|
|
38
|
+
React 是可选 peer dependency,只有从 `view-anchor/react` 导入时才需要。根入口(`view-anchor`)和 `view-anchor/protocol` 不依赖 React,未安装 React 也能加载。
|
|
47
39
|
|
|
48
|
-
##
|
|
40
|
+
## 使用
|
|
49
41
|
|
|
50
42
|
### 跟随一个 DOM 元素
|
|
51
43
|
|
|
52
44
|
```ts
|
|
53
45
|
import { createViewAnchor } from 'view-anchor'
|
|
54
46
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
const publish = (placement) => {
|
|
48
|
+
if (placement.visible) applyBoundsToExternalSurface(placement.bounds)
|
|
49
|
+
else hideExternalSurface()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handle = createViewAnchor(placeholderEl, {
|
|
53
|
+
visible: true,
|
|
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 让每次测量都发布,即使值没变
|
|
58
60
|
})
|
|
59
61
|
|
|
60
|
-
handle.update({
|
|
61
|
-
handle.
|
|
62
|
+
handle.update({ visible: true, publish }) // 更新选项并立即重新发布
|
|
63
|
+
handle.pulse() // 位置变了但没有任何观察器能感知时(如一次类名切换让目标移动了但尺寸没变)主动重测;稳定后自动停止
|
|
64
|
+
handle.dispose() // 停止观察;之后不会再发布
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
把 `
|
|
67
|
+
把 `visible` 设为 `false` 会发布 `{ visible: false }` 并停止观察。你的 `publish` 函数可以把它解释为隐藏、移除,或保留外部画面。
|
|
68
|
+
|
|
69
|
+
`createViewAnchor`、`createSizeAnchor` 和 `createGeometryBatcher` 都支持 `signal`。调用 `AbortController.abort()` 的清理效果等同于 `dispose()`;创建前已经 abort 的 signal 不会开始测量或安装监听。
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const controller = new AbortController()
|
|
73
|
+
const handle = createViewAnchor(placeholderEl, {
|
|
74
|
+
visible: true,
|
|
75
|
+
publish,
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
controller.abort()
|
|
80
|
+
```
|
|
65
81
|
|
|
66
82
|
### React
|
|
67
83
|
|
|
68
84
|
```tsx
|
|
69
85
|
import { useViewAnchor } from 'view-anchor/react'
|
|
70
86
|
|
|
71
|
-
function
|
|
87
|
+
function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
|
|
72
88
|
const ref = useViewAnchor({
|
|
73
|
-
|
|
74
|
-
publish:
|
|
89
|
+
visible,
|
|
90
|
+
publish: applyPlacementToExternalSurface,
|
|
91
|
+
followScroll: true,
|
|
92
|
+
followGeometry: true,
|
|
75
93
|
})
|
|
76
|
-
|
|
77
|
-
// 只会收起视图,不会销毁它。
|
|
94
|
+
|
|
78
95
|
return <div ref={ref} className="h-full w-full" />
|
|
79
96
|
}
|
|
80
97
|
```
|
|
81
98
|
|
|
82
|
-
Hook
|
|
99
|
+
元素卸载时,Hook 会先发布 `{ visible: false }`,再释放监听。它兼容 React 18 和 19 的 StrictMode 重挂载,不会因此发布过期帧。
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
它每次调用都会重新应用选项,规则与 `createViewAnchor` 和 `update()` 一致:省略任何一项都会重置为默认值,而不是沿用上次的值。省略 `treatZeroAreaAsHidden`、`followScroll` 或 `followGeometry` 会重置为 `false`;省略 `holdSelector` 会重置为 `[role="separator"]`;省略 `dedupe` 会重置为 `true`。要关闭 `holdSelector` / `dedupe`,传 `null` / `false`。
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
### 由内容决定占位尺寸
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
import { createPlacementAnchor } from 'view-anchor'
|
|
90
|
-
|
|
91
|
-
const handle = createPlacementAnchor(target, {
|
|
92
|
-
publish: (placement) => { ... },
|
|
93
|
-
followScroll: true, // 任意祖先滚动时重新测量
|
|
94
|
-
followGeometry: true, // 滚动 / 拖拽期间逐帧轮询,稳定后停止
|
|
95
|
-
guardDisplayNone: true, // 零面积或 display:none 的目标 → { visible: false }
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
handle.pulse() // 打开一小段逐帧跟随窗口,比如 CSS 过渡期间
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
React 版本是 `view-anchor/react` 里的 `usePlacementAnchor`。
|
|
102
|
-
|
|
103
|
-
### 由内容决定尺寸
|
|
104
|
-
|
|
105
|
-
有时被嵌入的那个画面的尺寸应该由它自己的内容决定,比如一条由下游代码渲染的工具栏。这种情况在被嵌入的文档里运行 `createSizeAdvertiser`,它把内容尺寸报告回去,宿主里的 DOM 占位元素就能跟着长到一样大:
|
|
105
|
+
当外部画面的内容尺寸需要反过来调整占位元素时,在内容所在的文档中使用 `createSizeAnchor`:
|
|
106
106
|
|
|
107
107
|
```ts
|
|
108
|
-
import {
|
|
108
|
+
import { createSizeAnchor } from 'view-anchor'
|
|
109
|
+
|
|
110
|
+
const publish = updatePlaceholderSize
|
|
109
111
|
|
|
110
|
-
const handle =
|
|
111
|
-
axis: 'block',
|
|
112
|
-
publish
|
|
112
|
+
const handle = createSizeAnchor(contentWrapper, {
|
|
113
|
+
axis: 'block', // 一个 size anchor 只负责一个轴:block = 高度,inline = 宽度
|
|
114
|
+
publish,
|
|
113
115
|
})
|
|
114
116
|
|
|
115
|
-
handle.update(publish) //
|
|
116
|
-
handle.dispose()
|
|
117
|
+
handle.update({ publish }) // 应用新选项并立即重新报告当前尺寸;省略 dedupe 会重置为 true
|
|
118
|
+
handle.dispose()
|
|
117
119
|
```
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
目标元素在它负责的轴上必须随内容伸缩。若应用也反过来在同一轴上强制设置尺寸,双方会互相触发布局,难以稳定。详见 [双向几何设计](./docs/bidirectional-design.md)。
|
|
120
122
|
|
|
121
|
-
###
|
|
123
|
+
### 需要传递、校验和排序时
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
核心只交付 `Bounds`、`Placement` 和 `SizeMeasurement`。如果应用需要把这些值交给别的页面、环境或异步通道,可选的 `view-anchor/protocol` 提供消息版本、输入校验、乱序过滤和微任务合并。
|
|
124
126
|
|
|
125
127
|
```ts
|
|
126
128
|
import {
|
|
@@ -129,57 +131,63 @@ import {
|
|
|
129
131
|
decodeGeometryWireValue,
|
|
130
132
|
} from 'view-anchor/protocol'
|
|
131
133
|
|
|
132
|
-
//
|
|
133
|
-
const batcher = createGeometryBatcher(
|
|
134
|
+
// sendGeometryBatch 由应用实现:接收一个 batch,并同步返回是否已接收。
|
|
135
|
+
const batcher = createGeometryBatcher(sendGeometryBatch)
|
|
134
136
|
const publish = createPlacementMessagePublisher(
|
|
135
137
|
{ anchorId: 'editor', generation: 3 },
|
|
136
138
|
batcher.publish,
|
|
137
139
|
)
|
|
138
140
|
|
|
139
|
-
// 接收方(主进程、宿主页面等)
|
|
140
141
|
const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
|
|
141
142
|
if (decoded.ok) {
|
|
142
|
-
|
|
143
|
+
// 先验证来源,再只应用更新的消息。
|
|
143
144
|
}
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
同一个 `{ anchorId, generation }` 应只保留一个 publisher 实例。针对同一地址重新创建 publisher 会让序号回到 1,接收端会把新消息当作过期消息丢弃。需要重新开始时,把 `generation` 加一。
|
|
148
|
+
|
|
149
|
+
完整约定见 [通信协议](./docs/protocol.md)。
|
|
150
|
+
|
|
151
|
+
## 性能边界
|
|
147
152
|
|
|
148
|
-
-
|
|
149
|
-
-
|
|
153
|
+
- 默认去重:与上一次已发布的 `Placement`/尺寸完全相同不会再次调用 `publish`;传 `dedupe: false` 可以让每次测量都发布。
|
|
154
|
+
- `followGeometry` 只在需要时启动动画帧轮询,稳定后停止。
|
|
155
|
+
- `createGeometryBatcher` 会合并同一微任务内的更新;每个锚点的位置和尺寸各保留最新值。
|
|
156
|
+
- `dispose()` 或 abort 会停止监听并释放长期持有的元素和回调引用。
|
|
150
157
|
|
|
151
|
-
|
|
158
|
+
与 0.2.2 的对比和导出体积见 [性能报告](./docs/performance-report.md)。
|
|
152
159
|
|
|
153
160
|
## API
|
|
154
161
|
|
|
155
|
-
| 导出
|
|
156
|
-
|
|
|
157
|
-
| `createViewAnchor(target, opts)`
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 才真正删除。
|
|
172
180
|
|
|
173
181
|
## 文档
|
|
174
182
|
|
|
175
|
-
- [
|
|
176
|
-
- [
|
|
177
|
-
- [
|
|
178
|
-
- [
|
|
183
|
+
- [机制说明](./docs/mechanism.md):正向测量的触发条件、默认去重与 `dedupe: false`、可见性与 React 生命周期。
|
|
184
|
+
- [双向几何设计](./docs/bidirectional-design.md):内容尺寸反向调整占位元素时的单轴约束。
|
|
185
|
+
- [通信协议](./docs/protocol.md):消息格式、校验、排序和批处理。
|
|
186
|
+
- [性能报告](./docs/performance-report.md):性能结论和导出体积。
|
|
179
187
|
|
|
180
188
|
## 参与贡献
|
|
181
189
|
|
|
182
|
-
|
|
190
|
+
提交前运行 `pnpm lint`、`pnpm format:check`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 用于更新性能报告。
|
|
183
191
|
|
|
184
192
|
## 许可证
|
|
185
193
|
|
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"}
|