view-anchor 0.2.2 → 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.
Files changed (46) hide show
  1. package/README.md +71 -87
  2. package/README.zh-CN.md +59 -64
  3. package/dist/index.d.ts +5 -7
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -3
  6. package/dist/protocol-publisher.d.ts +7 -9
  7. package/dist/protocol-publisher.d.ts.map +1 -1
  8. package/dist/protocol-publisher.js +2 -8
  9. package/dist/protocol-types.d.ts +5 -8
  10. package/dist/protocol-types.d.ts.map +1 -1
  11. package/dist/protocol-types.js +3 -6
  12. package/dist/protocol.d.ts +2 -2
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js +10 -2
  15. package/dist/react.d.ts +2 -15
  16. package/dist/react.d.ts.map +1 -1
  17. package/dist/react.js +43 -38
  18. package/dist/{size-advertiser.d.ts → size-anchor.d.ts} +6 -5
  19. package/dist/size-anchor.d.ts.map +1 -0
  20. package/dist/size-anchor.js +131 -0
  21. package/dist/types.d.ts +26 -32
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/view-anchor.d.ts +52 -33
  24. package/dist/view-anchor.d.ts.map +1 -1
  25. package/dist/view-anchor.js +186 -265
  26. package/docs/bidirectional-design.md +29 -34
  27. package/docs/index.html +656 -366
  28. package/docs/mechanism.md +35 -57
  29. package/docs/performance-report.md +12 -54
  30. package/docs/protocol.md +12 -3
  31. package/package.json +4 -6
  32. package/src/index.ts +5 -19
  33. package/src/protocol-publisher.ts +10 -16
  34. package/src/protocol-types.ts +5 -8
  35. package/src/protocol.ts +13 -7
  36. package/src/react.ts +51 -64
  37. package/src/size-anchor.ts +131 -0
  38. package/src/types.ts +29 -37
  39. package/src/view-anchor.ts +221 -287
  40. package/dist/measure-loop.d.ts +0 -24
  41. package/dist/measure-loop.d.ts.map +0 -1
  42. package/dist/measure-loop.js +0 -89
  43. package/dist/size-advertiser.d.ts.map +0 -1
  44. package/dist/size-advertiser.js +0 -94
  45. package/src/measure-loop.ts +0 -101
  46. package/src/size-advertiser.ts +0 -108
@@ -1,24 +0,0 @@
1
- /**
2
- * Internal helper: animation-frame scheduling and deduplication loop for
3
- * `createSizeAdvertiser`.
4
- *
5
- * Coalesces resize triggers into a single requestAnimationFrame, drops
6
- * duplicate measurements, and manages disposal.
7
- */
8
- export interface MeasureLoop<T> {
9
- schedule(): void;
10
- emitNow(value: T): void;
11
- setActive(on: boolean): void;
12
- cancel(): void;
13
- dispose(): void;
14
- }
15
- export declare function createMeasureLoop<T>(cfg: {
16
- /**
17
- * Produce the value to emit in the animation frame. Return null to skip
18
- * the frame (e.g. for non-finite measurements).
19
- */
20
- produce: () => T | null;
21
- same: (a: T, b: T) => boolean;
22
- sink: import('./types.js').Publisher<T>;
23
- }): MeasureLoop<T>;
24
- //# sourceMappingURL=measure-loop.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AASD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,OAAO,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACxC,GAAG,WAAW,CAAC,CAAC,CAAC,CAqEjB"}
@@ -1,89 +0,0 @@
1
- /**
2
- * Internal helper: animation-frame scheduling and deduplication loop for
3
- * `createSizeAdvertiser`.
4
- *
5
- * Coalesces resize triggers into a single requestAnimationFrame, drops
6
- * duplicate measurements, and manages disposal.
7
- */
8
- // Terminal-state stand-ins for cfg.produce/same/sink so a retained handle's
9
- // dispose()d loop does not keep the original closures (or what they
10
- // captured) alive. Guarded call sites never actually reach these.
11
- const NOOP_PRODUCE = () => null;
12
- const NOOP_SAME = () => true;
13
- const NOOP_SINK = () => false;
14
- export function createMeasureLoop(cfg) {
15
- let produce = cfg.produce;
16
- let same = cfg.same;
17
- let sink = cfg.sink;
18
- let rafId = null;
19
- let active = false;
20
- let disposed = false;
21
- let last = null;
22
- let publicationRevision = 0;
23
- const deliver = (value) => {
24
- // A reentrant dispose() from produce()/same() (invoked by frame() just
25
- // before this call) already cleared `last`; do not let this delivery
26
- // attempt write over that terminal state.
27
- if (disposed)
28
- return false;
29
- const previous = last;
30
- const attempt = ++publicationRevision;
31
- last = value;
32
- try {
33
- const accepted = sink(value) !== false;
34
- // A reentrant dispose() during sink() already cleared `last`; do not
35
- // resurrect the pre-dispose value over that terminal state.
36
- if (!accepted && publicationRevision === attempt && !disposed)
37
- last = previous;
38
- return accepted;
39
- }
40
- catch (error) {
41
- if (publicationRevision === attempt && !disposed)
42
- last = previous;
43
- throw error;
44
- }
45
- };
46
- const cancel = () => {
47
- if (rafId !== null) {
48
- cancelAnimationFrame(rafId);
49
- rafId = null;
50
- }
51
- };
52
- const frame = () => {
53
- rafId = null;
54
- if (disposed || !active)
55
- return;
56
- const value = produce();
57
- if (value === null)
58
- return;
59
- if (last !== null && same(value, last))
60
- return;
61
- deliver(value);
62
- };
63
- return {
64
- schedule() {
65
- if (disposed || !active || rafId !== null)
66
- return;
67
- rafId = requestAnimationFrame(frame);
68
- },
69
- emitNow(value) {
70
- if (disposed)
71
- return;
72
- deliver(value);
73
- },
74
- setActive(on) {
75
- active = on;
76
- },
77
- cancel,
78
- dispose() {
79
- if (disposed)
80
- return;
81
- disposed = true;
82
- cancel();
83
- produce = NOOP_PRODUCE;
84
- same = NOOP_SAME;
85
- sink = NOOP_SINK;
86
- last = null;
87
- },
88
- };
89
- }
@@ -1 +0,0 @@
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"}
@@ -1,94 +0,0 @@
1
- import { watchAbort } from './abort.js';
2
- import { createMeasureLoop } from './measure-loop.js';
3
- // Replaces a disposed instance's publish callback so a retained handle does
4
- // not keep the caller's original callback (and whatever it captured) alive.
5
- const NOOP_PUBLISH = () => false;
6
- /**
7
- * Report content size for a single axis back to the host.
8
- *
9
- * Runs in a downstream document, reads the content size from
10
- * `ResizeObserverEntry.borderBoxSize` without triggering reflow, and
11
- * publishes updates through an animation frame loop (`createMeasureLoop`).
12
- *
13
- * Measurements are rounded to integer pixels and clamped to >= 0.
14
- *
15
- * Note: `target` should be a shrink-to-fit wrapper on the owned axis.
16
- * If its size is driven by the host view itself (such as `<body>` or `<html>`),
17
- * updates will not shrink back to content size.
18
- */
19
- export function createSizeAdvertiser(target, opts) {
20
- const axis = opts.axis;
21
- let publish = opts.publish;
22
- let observer = null;
23
- let disposed = false;
24
- // Latest border-box recorded by the ResizeObserver callback.
25
- let latest = null;
26
- const produce = () => {
27
- if (!latest)
28
- return null;
29
- const raw = axis === 'block' ? latest.blockSize : latest.inlineSize;
30
- if (!Number.isFinite(raw))
31
- return null;
32
- return Math.max(0, Math.round(raw));
33
- };
34
- const loop = createMeasureLoop({
35
- produce,
36
- same: (a, b) => a === b,
37
- sink: (extent) => publish({ axis, extent }),
38
- });
39
- const onResize = (entries) => {
40
- // A callback queued before disconnect() can still fire once more; do not
41
- // let it write `latest` after dispose() has already cleared it.
42
- if (disposed)
43
- return;
44
- const entry = entries[entries.length - 1];
45
- if (entry) {
46
- latest = entry.borderBoxSize?.[0] ?? entry.contentBoxSize?.[0] ?? latest;
47
- }
48
- loop.schedule();
49
- };
50
- // Warn if measuring body or documentElement, whose size matches the view.
51
- const doc = target.ownerDocument;
52
- if (target === doc.body || target === doc.documentElement) {
53
- console.warn(`[view-anchor] size-advertiser: <${target === doc.body ? 'body' : 'html'}>'s ` +
54
- `${axis} size is the view size, not content size. The advertiser will ` +
55
- `never shrink to content; measure a shrink-to-fit wrapper instead.`);
56
- }
57
- let removeAbortListener = () => { };
58
- const dispose = () => {
59
- if (disposed)
60
- return;
61
- disposed = true;
62
- removeAbortListener();
63
- removeAbortListener = () => { };
64
- loop.cancel();
65
- if (observer) {
66
- observer.disconnect();
67
- observer = null;
68
- }
69
- loop.dispose();
70
- publish = NOOP_PUBLISH;
71
- latest = null;
72
- };
73
- if (opts.signal?.aborted)
74
- dispose();
75
- else {
76
- removeAbortListener = watchAbort(opts.signal, dispose);
77
- loop.setActive(true);
78
- observer = new ResizeObserver(onResize);
79
- observer.observe(target);
80
- }
81
- return {
82
- update(nextPublish) {
83
- if (disposed)
84
- return;
85
- publish = nextPublish;
86
- // Re-publish the current size to the new sink immediately so it is not
87
- // empty until the next ResizeObserver tick.
88
- const cur = produce();
89
- if (cur !== null)
90
- loop.emitNow(cur);
91
- },
92
- dispose,
93
- };
94
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Internal helper: animation-frame scheduling and deduplication loop for
3
- * `createSizeAdvertiser`.
4
- *
5
- * Coalesces resize triggers into a single requestAnimationFrame, drops
6
- * duplicate measurements, and manages disposal.
7
- */
8
-
9
- export interface MeasureLoop<T> {
10
- schedule(): void
11
- emitNow(value: T): void
12
- setActive(on: boolean): void
13
- cancel(): void
14
- dispose(): void
15
- }
16
-
17
- // Terminal-state stand-ins for cfg.produce/same/sink so a retained handle's
18
- // dispose()d loop does not keep the original closures (or what they
19
- // captured) alive. Guarded call sites never actually reach these.
20
- const NOOP_PRODUCE = (): null => null
21
- const NOOP_SAME = (): true => true
22
- const NOOP_SINK = (): false => false
23
-
24
- export function createMeasureLoop<T>(cfg: {
25
- /**
26
- * Produce the value to emit in the animation frame. Return null to skip
27
- * the frame (e.g. for non-finite measurements).
28
- */
29
- produce: () => T | null
30
- same: (a: T, b: T) => boolean
31
- sink: import('./types.js').Publisher<T>
32
- }): MeasureLoop<T> {
33
- let produce = cfg.produce
34
- let same = cfg.same
35
- let sink = cfg.sink
36
- let rafId: number | null = null
37
- let active = false
38
- let disposed = false
39
- let last: T | null = null
40
- let publicationRevision = 0
41
-
42
- const deliver = (value: T): boolean => {
43
- // A reentrant dispose() from produce()/same() (invoked by frame() just
44
- // before this call) already cleared `last`; do not let this delivery
45
- // attempt write over that terminal state.
46
- if (disposed) return false
47
- const previous = last
48
- const attempt = ++publicationRevision
49
- last = value
50
- try {
51
- const accepted = sink(value) !== false
52
- // A reentrant dispose() during sink() already cleared `last`; do not
53
- // resurrect the pre-dispose value over that terminal state.
54
- if (!accepted && publicationRevision === attempt && !disposed) last = previous
55
- return accepted
56
- } catch (error) {
57
- if (publicationRevision === attempt && !disposed) last = previous
58
- throw error
59
- }
60
- }
61
-
62
- const cancel = (): void => {
63
- if (rafId !== null) {
64
- cancelAnimationFrame(rafId)
65
- rafId = null
66
- }
67
- }
68
-
69
- const frame = (): void => {
70
- rafId = null
71
- if (disposed || !active) return
72
- const value = produce()
73
- if (value === null) return
74
- if (last !== null && same(value, last)) return
75
- deliver(value)
76
- }
77
-
78
- return {
79
- schedule(): void {
80
- if (disposed || !active || rafId !== null) return
81
- rafId = requestAnimationFrame(frame)
82
- },
83
- emitNow(value: T): void {
84
- if (disposed) return
85
- deliver(value)
86
- },
87
- setActive(on: boolean): void {
88
- active = on
89
- },
90
- cancel,
91
- dispose(): void {
92
- if (disposed) return
93
- disposed = true
94
- cancel()
95
- produce = NOOP_PRODUCE
96
- same = NOOP_SAME
97
- sink = NOOP_SINK
98
- last = null
99
- },
100
- }
101
- }
@@ -1,108 +0,0 @@
1
- import type {
2
- AdvertisedSize,
3
- Publisher,
4
- SizeAdvertiserOptions,
5
- SizeAdvertiserHandle,
6
- } from './types.js'
7
- import { watchAbort } from './abort.js'
8
- import { createMeasureLoop } from './measure-loop.js'
9
-
10
- // Replaces a disposed instance's publish callback so a retained handle does
11
- // not keep the caller's original callback (and whatever it captured) alive.
12
- const NOOP_PUBLISH = (): false => false
13
-
14
- /**
15
- * Report content size for a single axis back to the host.
16
- *
17
- * Runs in a downstream document, reads the content size from
18
- * `ResizeObserverEntry.borderBoxSize` without triggering reflow, and
19
- * publishes updates through an animation frame loop (`createMeasureLoop`).
20
- *
21
- * Measurements are rounded to integer pixels and clamped to >= 0.
22
- *
23
- * Note: `target` should be a shrink-to-fit wrapper on the owned axis.
24
- * If its size is driven by the host view itself (such as `<body>` or `<html>`),
25
- * updates will not shrink back to content size.
26
- */
27
- export function createSizeAdvertiser(
28
- target: HTMLElement,
29
- opts: SizeAdvertiserOptions,
30
- ): SizeAdvertiserHandle {
31
- const axis = opts.axis
32
- let publish = opts.publish
33
- let observer: ResizeObserver | null = null
34
- let disposed = false
35
- // Latest border-box recorded by the ResizeObserver callback.
36
- let latest: ResizeObserverSize | null = null
37
-
38
- const produce = (): number | null => {
39
- if (!latest) return null
40
- const raw = axis === 'block' ? latest.blockSize : latest.inlineSize
41
- if (!Number.isFinite(raw)) return null
42
- return Math.max(0, Math.round(raw))
43
- }
44
-
45
- const loop = createMeasureLoop<number>({
46
- produce,
47
- same: (a, b) => a === b,
48
- sink: (extent) => publish({ axis, extent }),
49
- })
50
-
51
- const onResize: ResizeObserverCallback = (entries) => {
52
- // A callback queued before disconnect() can still fire once more; do not
53
- // let it write `latest` after dispose() has already cleared it.
54
- if (disposed) return
55
- const entry = entries[entries.length - 1]
56
- if (entry) {
57
- latest = entry.borderBoxSize?.[0] ?? entry.contentBoxSize?.[0] ?? latest
58
- }
59
- loop.schedule()
60
- }
61
-
62
- // Warn if measuring body or documentElement, whose size matches the view.
63
- const doc = target.ownerDocument
64
- if (target === doc.body || target === doc.documentElement) {
65
- console.warn(
66
- `[view-anchor] size-advertiser: <${target === doc.body ? 'body' : 'html'}>'s ` +
67
- `${axis} size is the view size, not content size. The advertiser will ` +
68
- `never shrink to content; measure a shrink-to-fit wrapper instead.`,
69
- )
70
- }
71
-
72
- let removeAbortListener = (): void => {}
73
-
74
- const dispose = (): void => {
75
- if (disposed) return
76
- disposed = true
77
- removeAbortListener()
78
- removeAbortListener = (): void => {}
79
- loop.cancel()
80
- if (observer) {
81
- observer.disconnect()
82
- observer = null
83
- }
84
- loop.dispose()
85
- publish = NOOP_PUBLISH
86
- latest = null
87
- }
88
-
89
- if (opts.signal?.aborted) dispose()
90
- else {
91
- removeAbortListener = watchAbort(opts.signal, dispose)
92
- loop.setActive(true)
93
- observer = new ResizeObserver(onResize)
94
- observer.observe(target)
95
- }
96
-
97
- return {
98
- update(nextPublish: Publisher<AdvertisedSize>): void {
99
- if (disposed) return
100
- publish = nextPublish
101
- // Re-publish the current size to the new sink immediately so it is not
102
- // empty until the next ResizeObserver tick.
103
- const cur = produce()
104
- if (cur !== null) loop.emitNow(cur)
105
- },
106
- dispose,
107
- }
108
- }