@wardic/dom 0.0.0-stage → 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +145 -0
- package/dist/Appear.d.ts +24 -0
- package/dist/Appear.d.ts.map +1 -0
- package/dist/Appear.js +25 -0
- package/dist/Appear.js.map +1 -0
- package/dist/Color.d.ts +14 -0
- package/dist/Color.d.ts.map +1 -0
- package/dist/Color.js +16 -0
- package/dist/Color.js.map +1 -0
- package/dist/DomAnnouncer.d.ts +13 -0
- package/dist/DomAnnouncer.d.ts.map +1 -0
- package/dist/DomAnnouncer.js +14 -0
- package/dist/DomAnnouncer.js.map +1 -0
- package/dist/DomClock.d.ts +11 -0
- package/dist/DomClock.d.ts.map +1 -0
- package/dist/DomClock.js +12 -0
- package/dist/DomClock.js.map +1 -0
- package/dist/DomDefaultTransition.d.ts +12 -0
- package/dist/DomDefaultTransition.d.ts.map +1 -0
- package/dist/DomDefaultTransition.js +13 -0
- package/dist/DomDefaultTransition.js.map +1 -0
- package/dist/DomInput.d.ts +13 -0
- package/dist/DomInput.d.ts.map +1 -0
- package/dist/DomInput.js +15 -0
- package/dist/DomInput.js.map +1 -0
- package/dist/DomInterpolators.d.ts +13 -0
- package/dist/DomInterpolators.d.ts.map +1 -0
- package/dist/DomInterpolators.js +14 -0
- package/dist/DomInterpolators.js.map +1 -0
- package/dist/DomKeyRules.d.ts +13 -0
- package/dist/DomKeyRules.d.ts.map +1 -0
- package/dist/DomKeyRules.js +14 -0
- package/dist/DomKeyRules.js.map +1 -0
- package/dist/DomPositionalKeys.d.ts +13 -0
- package/dist/DomPositionalKeys.d.ts.map +1 -0
- package/dist/DomPositionalKeys.js +14 -0
- package/dist/DomPositionalKeys.js.map +1 -0
- package/dist/DomProbe.d.ts +13 -0
- package/dist/DomProbe.d.ts.map +1 -0
- package/dist/DomProbe.js +14 -0
- package/dist/DomProbe.js.map +1 -0
- package/dist/DomProjectionSystem.d.ts +28 -0
- package/dist/DomProjectionSystem.d.ts.map +1 -0
- package/dist/DomProjectionSystem.js +41 -0
- package/dist/DomProjectionSystem.js.map +1 -0
- package/dist/DomRenderer.d.ts +13 -0
- package/dist/DomRenderer.d.ts.map +1 -0
- package/dist/DomRenderer.js +15 -0
- package/dist/DomRenderer.js.map +1 -0
- package/dist/FlushGate.d.ts +48 -0
- package/dist/FlushGate.d.ts.map +1 -0
- package/dist/FlushGate.js +62 -0
- package/dist/FlushGate.js.map +1 -0
- package/dist/MeasuredPair.d.ts +14 -0
- package/dist/MeasuredPair.d.ts.map +1 -0
- package/dist/MeasuredPair.js +16 -0
- package/dist/MeasuredPair.js.map +1 -0
- package/dist/OwnValues.d.ts +14 -0
- package/dist/OwnValues.d.ts.map +1 -0
- package/dist/OwnValues.js +15 -0
- package/dist/OwnValues.js.map +1 -0
- package/dist/ProjectionSystem.d.ts +34 -0
- package/dist/ProjectionSystem.d.ts.map +1 -0
- package/dist/ProjectionSystem.js +29 -0
- package/dist/ProjectionSystem.js.map +1 -0
- package/dist/ReorderController.d.ts +41 -0
- package/dist/ReorderController.d.ts.map +1 -0
- package/dist/ReorderController.js +44 -0
- package/dist/ReorderController.js.map +1 -0
- package/dist/Report.d.ts +12 -0
- package/dist/Report.d.ts.map +1 -0
- package/dist/Report.js +13 -0
- package/dist/Report.js.map +1 -0
- package/dist/ScrollOffset.d.ts +34 -0
- package/dist/ScrollOffset.d.ts.map +1 -0
- package/dist/ScrollOffset.js +42 -0
- package/dist/ScrollOffset.js.map +1 -0
- package/dist/Style.d.ts +15 -0
- package/dist/Style.d.ts.map +1 -0
- package/dist/Style.js +17 -0
- package/dist/Style.js.map +1 -0
- package/dist/Svg.d.ts +13 -0
- package/dist/Svg.d.ts.map +1 -0
- package/dist/Svg.js +14 -0
- package/dist/Svg.js.map +1 -0
- package/dist/Variables.d.ts +13 -0
- package/dist/Variables.d.ts.map +1 -0
- package/dist/Variables.js +14 -0
- package/dist/Variables.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/announcer.d.ts +50 -0
- package/dist/internal/announcer.d.ts.map +1 -0
- package/dist/internal/announcer.js +115 -0
- package/dist/internal/announcer.js.map +1 -0
- package/dist/internal/appear.d.ts +169 -0
- package/dist/internal/appear.d.ts.map +1 -0
- package/dist/internal/appear.js +499 -0
- package/dist/internal/appear.js.map +1 -0
- package/dist/internal/auto-scroll.d.ts +24 -0
- package/dist/internal/auto-scroll.d.ts.map +1 -0
- package/dist/internal/auto-scroll.js +124 -0
- package/dist/internal/auto-scroll.js.map +1 -0
- package/dist/internal/clock.d.ts +19 -0
- package/dist/internal/clock.d.ts.map +1 -0
- package/dist/internal/clock.js +39 -0
- package/dist/internal/clock.js.map +1 -0
- package/dist/internal/colors.d.ts +45 -0
- package/dist/internal/colors.d.ts.map +1 -0
- package/dist/internal/colors.js +193 -0
- package/dist/internal/colors.js.map +1 -0
- package/dist/internal/complex.d.ts +24 -0
- package/dist/internal/complex.d.ts.map +1 -0
- package/dist/internal/complex.js +28 -0
- package/dist/internal/complex.js.map +1 -0
- package/dist/internal/content-watch.d.ts +20 -0
- package/dist/internal/content-watch.d.ts.map +1 -0
- package/dist/internal/content-watch.js +76 -0
- package/dist/internal/content-watch.js.map +1 -0
- package/dist/internal/default-transition.d.ts +25 -0
- package/dist/internal/default-transition.d.ts.map +1 -0
- package/dist/internal/default-transition.js +41 -0
- package/dist/internal/default-transition.js.map +1 -0
- package/dist/internal/drag-defaults.d.ts +18 -0
- package/dist/internal/drag-defaults.d.ts.map +1 -0
- package/dist/internal/drag-defaults.js +38 -0
- package/dist/internal/drag-defaults.js.map +1 -0
- package/dist/internal/gate.d.ts +186 -0
- package/dist/internal/gate.d.ts.map +1 -0
- package/dist/internal/gate.js +453 -0
- package/dist/internal/gate.js.map +1 -0
- package/dist/internal/input.d.ts +41 -0
- package/dist/internal/input.d.ts.map +1 -0
- package/dist/internal/input.js +476 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/interpolators.d.ts +19 -0
- package/dist/internal/interpolators.d.ts.map +1 -0
- package/dist/internal/interpolators.js +30 -0
- package/dist/internal/interpolators.js.map +1 -0
- package/dist/internal/key-rules.d.ts +11 -0
- package/dist/internal/key-rules.d.ts.map +1 -0
- package/dist/internal/key-rules.js +244 -0
- package/dist/internal/key-rules.js.map +1 -0
- package/dist/internal/measure.d.ts +150 -0
- package/dist/internal/measure.d.ts.map +1 -0
- package/dist/internal/measure.js +278 -0
- package/dist/internal/measure.js.map +1 -0
- package/dist/internal/measured-pairs.d.ts +38 -0
- package/dist/internal/measured-pairs.d.ts.map +1 -0
- package/dist/internal/measured-pairs.js +251 -0
- package/dist/internal/measured-pairs.js.map +1 -0
- package/dist/internal/own-values.d.ts +35 -0
- package/dist/internal/own-values.d.ts.map +1 -0
- package/dist/internal/own-values.js +117 -0
- package/dist/internal/own-values.js.map +1 -0
- package/dist/internal/pass-ends.d.ts +21 -0
- package/dist/internal/pass-ends.d.ts.map +1 -0
- package/dist/internal/pass-ends.js +14 -0
- package/dist/internal/pass-ends.js.map +1 -0
- package/dist/internal/positional-keys.d.ts +16 -0
- package/dist/internal/positional-keys.d.ts.map +1 -0
- package/dist/internal/positional-keys.js +21 -0
- package/dist/internal/positional-keys.js.map +1 -0
- package/dist/internal/probe.d.ts +10 -0
- package/dist/internal/probe.d.ts.map +1 -0
- package/dist/internal/probe.js +117 -0
- package/dist/internal/probe.js.map +1 -0
- package/dist/internal/projection-defaults.d.ts +34 -0
- package/dist/internal/projection-defaults.d.ts.map +1 -0
- package/dist/internal/projection-defaults.js +21 -0
- package/dist/internal/projection-defaults.js.map +1 -0
- package/dist/internal/projection.d.ts +529 -0
- package/dist/internal/projection.d.ts.map +1 -0
- package/dist/internal/projection.js +4990 -0
- package/dist/internal/projection.js.map +1 -0
- package/dist/internal/renderer.d.ts +86 -0
- package/dist/internal/renderer.d.ts.map +1 -0
- package/dist/internal/renderer.js +616 -0
- package/dist/internal/renderer.js.map +1 -0
- package/dist/internal/reorder.d.ts +159 -0
- package/dist/internal/reorder.d.ts.map +1 -0
- package/dist/internal/reorder.js +437 -0
- package/dist/internal/reorder.js.map +1 -0
- package/dist/internal/report.d.ts +30 -0
- package/dist/internal/report.d.ts.map +1 -0
- package/dist/internal/report.js +60 -0
- package/dist/internal/report.js.map +1 -0
- package/dist/internal/require-global.d.ts +11 -0
- package/dist/internal/require-global.d.ts.map +1 -0
- package/dist/internal/require-global.js +31 -0
- package/dist/internal/require-global.js.map +1 -0
- package/dist/internal/reveal.d.ts +16 -0
- package/dist/internal/reveal.d.ts.map +1 -0
- package/dist/internal/reveal.js +66 -0
- package/dist/internal/reveal.js.map +1 -0
- package/dist/internal/runner.d.ts +21 -0
- package/dist/internal/runner.d.ts.map +1 -0
- package/dist/internal/runner.js +48 -0
- package/dist/internal/runner.js.map +1 -0
- package/dist/internal/scale-correction.d.ts +80 -0
- package/dist/internal/scale-correction.d.ts.map +1 -0
- package/dist/internal/scale-correction.js +227 -0
- package/dist/internal/scale-correction.js.map +1 -0
- package/dist/internal/scroll.d.ts +35 -0
- package/dist/internal/scroll.d.ts.map +1 -0
- package/dist/internal/scroll.js +244 -0
- package/dist/internal/scroll.js.map +1 -0
- package/dist/internal/scrollers.d.ts +35 -0
- package/dist/internal/scrollers.d.ts.map +1 -0
- package/dist/internal/scrollers.js +38 -0
- package/dist/internal/scrollers.js.map +1 -0
- package/dist/internal/starting-style.d.ts +141 -0
- package/dist/internal/starting-style.d.ts.map +1 -0
- package/dist/internal/starting-style.js +300 -0
- package/dist/internal/starting-style.js.map +1 -0
- package/dist/internal/svg.d.ts +60 -0
- package/dist/internal/svg.d.ts.map +1 -0
- package/dist/internal/svg.js +257 -0
- package/dist/internal/svg.js.map +1 -0
- package/dist/internal/variables.d.ts +45 -0
- package/dist/internal/variables.d.ts.map +1 -0
- package/dist/internal/variables.js +92 -0
- package/dist/internal/variables.js.map +1 -0
- package/package.json +65 -4
- package/src/Appear.ts +45 -0
- package/src/Color.ts +24 -0
- package/src/DomAnnouncer.ts +22 -0
- package/src/DomClock.ts +13 -0
- package/src/DomDefaultTransition.ts +16 -0
- package/src/DomInput.ts +22 -0
- package/src/DomInterpolators.ts +16 -0
- package/src/DomKeyRules.ts +17 -0
- package/src/DomPositionalKeys.ts +16 -0
- package/src/DomProbe.ts +15 -0
- package/src/DomProjectionSystem.ts +44 -0
- package/src/DomRenderer.ts +26 -0
- package/src/FlushGate.ts +81 -0
- package/src/MeasuredPair.ts +15 -0
- package/src/OwnValues.ts +19 -0
- package/src/ProjectionSystem.ts +51 -0
- package/src/ReorderController.ts +72 -0
- package/src/Report.ts +17 -0
- package/src/ScrollOffset.ts +54 -0
- package/src/Style.ts +37 -0
- package/src/Svg.ts +21 -0
- package/src/Variables.ts +17 -0
- package/src/index.ts +106 -0
- package/src/internal/announcer.ts +132 -0
- package/src/internal/appear.ts +739 -0
- package/src/internal/auto-scroll.ts +176 -0
- package/src/internal/clock.ts +45 -0
- package/src/internal/colors.ts +258 -0
- package/src/internal/complex.ts +29 -0
- package/src/internal/content-watch.ts +84 -0
- package/src/internal/culori.d.ts +58 -0
- package/src/internal/default-transition.ts +50 -0
- package/src/internal/drag-defaults.ts +54 -0
- package/src/internal/gate.ts +593 -0
- package/src/internal/input.ts +579 -0
- package/src/internal/interpolators.ts +33 -0
- package/src/internal/key-rules.ts +357 -0
- package/src/internal/measure.ts +357 -0
- package/src/internal/measured-pairs.ts +297 -0
- package/src/internal/own-values.ts +166 -0
- package/src/internal/pass-ends.ts +23 -0
- package/src/internal/positional-keys.ts +24 -0
- package/src/internal/probe.ts +176 -0
- package/src/internal/projection-defaults.ts +45 -0
- package/src/internal/projection.ts +5999 -0
- package/src/internal/renderer.ts +646 -0
- package/src/internal/reorder.ts +604 -0
- package/src/internal/report.ts +69 -0
- package/src/internal/require-global.ts +46 -0
- package/src/internal/reveal.ts +69 -0
- package/src/internal/runner.ts +64 -0
- package/src/internal/scale-correction.ts +310 -0
- package/src/internal/scroll.ts +356 -0
- package/src/internal/scrollers.ts +51 -0
- package/src/internal/starting-style.ts +396 -0
- package/src/internal/svg.ts +300 -0
- package/src/internal/variables.ts +145 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Probe.scroll` for the DOM (#234, ADR-0043 *Amendment (#233, #234)*): a container's scroll as an
|
|
3
|
+
* Atom, set inside the container's `scroll` event, the window's `resize`, and a resize of the
|
|
4
|
+
* container or its content (#239), so a value derived from a scroll is written in the frame that
|
|
5
|
+
* shows it (#45's pattern for `viewport`).
|
|
6
|
+
*
|
|
7
|
+
* One listener set a container, however many read it: a reader adds its own measure to the
|
|
8
|
+
* container's set and takes it out when its Scope closes, and the last one out removes the
|
|
9
|
+
* listeners. An `RcMap` of containers, not a `SharedSource`: no pump, so a reader sets its Atom
|
|
10
|
+
* inside the event, where `SharedSource`'s pump would set it a task after.
|
|
11
|
+
*
|
|
12
|
+
* A reader with a `target` or an `offset` reads its progress on `axis` through Motion's offsets
|
|
13
|
+
* (`ScrollOffset`, the DOM's syntax; `render/dom/scroll/offsets` in framer-motion 13.4.0, ported):
|
|
14
|
+
* each stop resolves to a scroll
|
|
15
|
+
* position, the target's edge (in the target's length, from where it sits in the container) less
|
|
16
|
+
* the container's edge (in the container's), and progress runs piecewise between them, clamped.
|
|
17
|
+
*
|
|
18
|
+
* **The content's size** (#239, ADR-0043 *Amendment (#239)*): content that grows or shrinks with the
|
|
19
|
+
* scroll still changes the range and a target's offsets, and fires no `scroll`. One `ResizeObserver`
|
|
20
|
+
* a container watches its own box and its direct children (the page: the body and its children),
|
|
21
|
+
* a `MutationObserver` keeping the children watched as they're added and removed, and each reader's
|
|
22
|
+
* target. A deep change resizes the direct child it sits in. Nothing runs while nothing changes; a
|
|
23
|
+
* change is read after its layout, so what it moves paints a frame later. Overflow no watched box
|
|
24
|
+
* shows (an absolutely positioned descendant hanging out) isn't seen: Motion's `trackContentSize`
|
|
25
|
+
* reads the size every frame instead, opt-in.
|
|
26
|
+
*
|
|
27
|
+
* Observability: per event, so quiet (no span, no log).
|
|
28
|
+
*/
|
|
29
|
+
import type * as Probe from "@wardic/core/Probe"
|
|
30
|
+
import * as Arr from "effect/Array"
|
|
31
|
+
import * as Bool from "effect/Boolean"
|
|
32
|
+
import * as Effect from "effect/Effect"
|
|
33
|
+
import * as Equal from "effect/Equal"
|
|
34
|
+
import * as Iter from "effect/Iterable"
|
|
35
|
+
import * as Match from "effect/Match"
|
|
36
|
+
import * as Option from "effect/Option"
|
|
37
|
+
import * as Order from "effect/Order"
|
|
38
|
+
import * as Predicate from "effect/Predicate"
|
|
39
|
+
import * as RcMap from "effect/RcMap"
|
|
40
|
+
import { Atom, type AtomRegistry } from "effect/reactivity"
|
|
41
|
+
import * as Rec from "effect/Record"
|
|
42
|
+
import type * as Scope from "effect/Scope"
|
|
43
|
+
import * as Str from "effect/String"
|
|
44
|
+
import * as UndefinedOr from "effect/UndefinedOr"
|
|
45
|
+
import * as ScrollOffset from "../ScrollOffset.ts"
|
|
46
|
+
import { counted, watchChildren, watchPage } from "./content-watch.ts"
|
|
47
|
+
|
|
48
|
+
/** A container's listeners and the readers they measure for. */
|
|
49
|
+
interface Container {
|
|
50
|
+
readonly readers: Set<() => void>
|
|
51
|
+
/** Watch a reader's target's size too, until the returned function is called. */
|
|
52
|
+
readonly watch: (target: Element) => () => void
|
|
53
|
+
readonly remove: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A position on two axes. */
|
|
57
|
+
interface Point {
|
|
58
|
+
readonly x: number
|
|
59
|
+
readonly y: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Motion's named edges, as fractions of a length. */
|
|
63
|
+
const NAMED_EDGES: Readonly<Record<string, number>> = { start: 0, center: 0.5, end: 1 }
|
|
64
|
+
|
|
65
|
+
/** Whether a string is one of Motion's named edges. */
|
|
66
|
+
const isNamed = (edge: string): boolean => Object.hasOwn(NAMED_EDGES, edge)
|
|
67
|
+
|
|
68
|
+
/** One edge and the length it's a part of. */
|
|
69
|
+
interface Edge {
|
|
70
|
+
readonly edge: ScrollOffset.Edge
|
|
71
|
+
readonly length: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* How far along `length` an edge sits: Motion's `resolveEdge`, less its inset. A name or a
|
|
76
|
+
* number is a fraction of the length; `px` is a distance, `%` a fraction, `vw` / `vh` a share of
|
|
77
|
+
* the viewport; any other string a fraction. Built once, at module load: per event per reader.
|
|
78
|
+
*/
|
|
79
|
+
const edgeOffset: (edge: Edge) => number = Match.type<Edge>().pipe(
|
|
80
|
+
Match.when({ edge: Match.number }, ({ edge, length }) => length * edge),
|
|
81
|
+
Match.when({ edge: (e: ScrollOffset.Edge) => Predicate.isString(e) && isNamed(e) }, ({ edge, length }) =>
|
|
82
|
+
length * Option.getOrElse(Rec.get(NAMED_EDGES, String(edge)), () => 0)),
|
|
83
|
+
Match.when({ edge: (e: ScrollOffset.Edge) => Predicate.isString(e) && Str.endsWith("px")(e) }, ({ edge }) => Number.parseFloat(String(edge))),
|
|
84
|
+
Match.when({ edge: (e: ScrollOffset.Edge) => Predicate.isString(e) && Str.endsWith("%")(e) }, ({ edge, length }) =>
|
|
85
|
+
(length * Number.parseFloat(String(edge))) / 100),
|
|
86
|
+
Match.when({ edge: (e: ScrollOffset.Edge) => Predicate.isString(e) && Str.endsWith("vw")(e) }, ({ edge }) =>
|
|
87
|
+
(Number.parseFloat(String(edge)) / 100) * document.documentElement.clientWidth),
|
|
88
|
+
Match.when({ edge: (e: ScrollOffset.Edge) => Predicate.isString(e) && Str.endsWith("vh")(e) }, ({ edge }) =>
|
|
89
|
+
(Number.parseFloat(String(edge)) / 100) * document.documentElement.clientHeight),
|
|
90
|
+
Match.orElse(({ edge, length }) => length * Number.parseFloat(String(edge))),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** A stop's two edges, the target's then the container's. */
|
|
94
|
+
type Pair = readonly [ScrollOffset.Edge, ScrollOffset.Edge]
|
|
95
|
+
|
|
96
|
+
/** A one-string stop's edges: two with a space, else a name for both, else the target's against the container's start. */
|
|
97
|
+
const pairOfString = (stop: string): Pair =>
|
|
98
|
+
Bool.match(Str.includes(" ")(stop), {
|
|
99
|
+
onTrue: (): Pair => {
|
|
100
|
+
const [target = "0", container = "0"] = Str.split(stop, " ")
|
|
101
|
+
return [target, container]
|
|
102
|
+
},
|
|
103
|
+
onFalse: (): Pair => [stop, Bool.match(isNamed(stop), { onTrue: () => stop, onFalse: () => "0" })],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/** A stop's edges (Motion's `resolveOffset`): a pair as it is, a number for both, a string as {@link pairOfString} reads it. */
|
|
107
|
+
const pairOf: (stop: ScrollOffset.Stop) => Pair = Match.type<ScrollOffset.Stop>().pipe(
|
|
108
|
+
Match.when(Match.number, (n): Pair => [n, n]),
|
|
109
|
+
Match.when(Match.string, (s) => pairOfString(Str.trim(s))),
|
|
110
|
+
Match.orElse((pair) => pair as Pair),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
/** A stop as a scroll position: where the target's edge meets the container's. */
|
|
114
|
+
const positionOf = (stop: ScrollOffset.Stop, containerLength: number, targetLength: number, targetInset: number): number => {
|
|
115
|
+
const [target, container] = pairOf(stop)
|
|
116
|
+
return targetInset + edgeOffset({ edge: target, length: targetLength }) - edgeOffset({ edge: container, length: containerLength })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A scroll position and the progress it stands for. */
|
|
120
|
+
interface Mark {
|
|
121
|
+
readonly at: number
|
|
122
|
+
readonly progress: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const byPosition: Order.Order<Mark> = Order.mapInput(Order.Number, (mark: Mark) => mark.at)
|
|
126
|
+
|
|
127
|
+
const clamp01 = (v: number): number => Math.min(1, Math.max(0, v))
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Progress at `current` through the scroll positions `stops`, 0 at the first and 1 at the last,
|
|
131
|
+
* evenly between them, clamped: Motion's `interpolate(stops, defaultOffset, { clamp: false })`
|
|
132
|
+
* then `clamp(0, 1)`. Descending stops read as Motion's interpolate reads them, reversed.
|
|
133
|
+
*/
|
|
134
|
+
const progressThrough = (stops: ReadonlyArray<number>, current: number): number => {
|
|
135
|
+
const marks = Arr.sort(
|
|
136
|
+
Arr.map(stops, (at, i): Mark => ({ at, progress: i / Math.max(1, stops.length - 1) })),
|
|
137
|
+
byPosition,
|
|
138
|
+
)
|
|
139
|
+
return Option.match(Arr.matchLeft(marks, { onEmpty: () => Option.none<Mark>(), onNonEmpty: (head) => Option.some(head) }), {
|
|
140
|
+
onNone: () => 0,
|
|
141
|
+
onSome: (first) => {
|
|
142
|
+
const last = Option.getOrElse(Arr.last(marks), () => first)
|
|
143
|
+
// Within the range: the segment `current` falls in, blended; outside it, the end it's past.
|
|
144
|
+
const segment = Arr.findFirst(Arr.zip(marks, Arr.drop(marks, 1)), ([, to]) => current <= to.at)
|
|
145
|
+
return Bool.match(current <= first.at, {
|
|
146
|
+
onTrue: () => clamp01(first.progress),
|
|
147
|
+
onFalse: () =>
|
|
148
|
+
Option.match(segment, {
|
|
149
|
+
onNone: () => clamp01(last.progress),
|
|
150
|
+
onSome: ([from, to]) =>
|
|
151
|
+
clamp01(
|
|
152
|
+
from.progress +
|
|
153
|
+
(to.progress - from.progress) * Bool.match(to.at === from.at, { onTrue: () => 1, onFalse: () => (current - from.at) / (to.at - from.at) }),
|
|
154
|
+
),
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Where `element` sits in `container`: Motion's `calcInset`, the offset chain up to it, SVG included.
|
|
163
|
+
* A `position: static` container is no offset parent, so the chain passes it (#395): where it has
|
|
164
|
+
* left the container, the container's own offset to the same parent comes off ({@link fromContainer}).
|
|
165
|
+
* A chain that ends instead measures from the document, or for a fixed target the viewport, in any
|
|
166
|
+
* container, as Motion's does: the page's own chain ends so, and stays as it is.
|
|
167
|
+
*/
|
|
168
|
+
const insetOf = (element: Element | null, container: Element, x: number, y: number): Point => {
|
|
169
|
+
if (Bool.or(element === null, element === container)) return { x, y }
|
|
170
|
+
const current = element as Element
|
|
171
|
+
// Per scroll event, and per step of the chain: statements, no matcher or Option built (#618, F6).
|
|
172
|
+
if (Bool.not(container.contains(current))) return fromContainer(container, current, x, y)
|
|
173
|
+
if (isHTMLElement(current)) return insetOf(current.offsetParent, container, x + current.offsetLeft, y + current.offsetTop)
|
|
174
|
+
if (current.tagName === "svg") {
|
|
175
|
+
const parent = current.parentElement
|
|
176
|
+
if (parent === null) return { x, y }
|
|
177
|
+
const own = current.getBoundingClientRect()
|
|
178
|
+
const box = parent.getBoundingClientRect()
|
|
179
|
+
return insetOf(parent, container, x + own.left - box.left, y + own.top - box.top)
|
|
180
|
+
}
|
|
181
|
+
if (current instanceof SVGGraphicsElement) {
|
|
182
|
+
const box = current.getBBox()
|
|
183
|
+
return insetOf(current.ownerSVGElement, container, x + box.x, y + box.y)
|
|
184
|
+
}
|
|
185
|
+
return { x, y }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isHTMLElement = (e: Element): e is HTMLElement => e instanceof HTMLElement
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* `x`, `y` from `parent`, where the offset chain left `container`, made `container`'s: its own offset
|
|
192
|
+
* to `parent` and its border taken off (#395). A static container's descendants measure from its
|
|
193
|
+
* nearest positioned ancestor (or `<body>`, or a table cell), as it does itself, and an `offsetTop`
|
|
194
|
+
* ignores its scroll, as a positioned container's descendants' do: this is the inset a positioned one
|
|
195
|
+
* gives.
|
|
196
|
+
*/
|
|
197
|
+
const fromContainer = (container: Element, parent: Element, x: number, y: number): Point => {
|
|
198
|
+
const own = offsetTo(container, parent, 0, 0)
|
|
199
|
+
return { x: x - own.x - container.clientLeft, y: y - own.y - container.clientTop }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** `element`'s offset along its offset chain up to `ancestor` (its end, for `null`). */
|
|
203
|
+
const offsetTo = (element: Element | null, ancestor: Element | null, x: number, y: number): Point => {
|
|
204
|
+
if (element === ancestor) return { x, y }
|
|
205
|
+
return Option.match(Option.filter(Option.fromNullOr(element), isHTMLElement), {
|
|
206
|
+
onNone: () => ({ x, y }),
|
|
207
|
+
onSome: (e) => offsetTo(e.offsetParent, ancestor, x + e.offsetLeft, y + e.offsetTop),
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A target's size: an SVG shape's box, else its client size (Motion's `getTargetSize`). */
|
|
212
|
+
const sizeOf = (target: Element): { readonly width: number; readonly height: number } =>
|
|
213
|
+
Bool.match(Bool.and(target instanceof SVGGraphicsElement, target.tagName !== "svg"), {
|
|
214
|
+
onTrue: () => (target as SVGGraphicsElement).getBBox(),
|
|
215
|
+
onFalse: () => ({ width: target.clientWidth, height: target.clientHeight }),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
/** A container's progress on one axis with no target or offset: Motion's `progress(0, length, current)`, 1 for no range. */
|
|
219
|
+
const plainProgress = (current: number, length: number): number => Bool.match(length === 0, { onTrue: () => 1, onFalse: () => current / length })
|
|
220
|
+
|
|
221
|
+
/** What a reader measures, for `options` on `element`. Reads layout: called in events, never per frame. */
|
|
222
|
+
const measureOf = (element: Element, options: Probe.ScrollOptions): (() => Probe.Scroll) => {
|
|
223
|
+
const { axis = "y" } = options
|
|
224
|
+
// The DOM's offset (core passes it through as the Environment's own); `All` with a target and none.
|
|
225
|
+
const offset = UndefinedOr.match(options.offset as ScrollOffset.ScrollOffset | undefined, {
|
|
226
|
+
onUndefined: () => ScrollOffset.All,
|
|
227
|
+
onDefined: (given) => given,
|
|
228
|
+
})
|
|
229
|
+
const target = UndefinedOr.match(options.target as Element | undefined, { onUndefined: () => element, onDefined: (given) => given })
|
|
230
|
+
// A target's progress, or an offset's through the container itself; else the plain one.
|
|
231
|
+
const resolves = Bool.or(Predicate.isNotUndefined(options.target), Predicate.isNotUndefined(options.offset))
|
|
232
|
+
const onAxis = (current: number): number => {
|
|
233
|
+
const own = target === element
|
|
234
|
+
const inset = Bool.match(own, { onTrue: (): Point => ({ x: 0, y: 0 }), onFalse: () => insetOf(target, element, 0, 0) })
|
|
235
|
+
const size = Bool.match(own, {
|
|
236
|
+
onTrue: () => ({ width: element.scrollWidth, height: element.scrollHeight }),
|
|
237
|
+
onFalse: () => sizeOf(target),
|
|
238
|
+
})
|
|
239
|
+
const y = axis === "y"
|
|
240
|
+
const containerLength = Bool.match(y, { onTrue: () => element.clientHeight, onFalse: () => element.clientWidth })
|
|
241
|
+
const targetLength = Bool.match(y, { onTrue: () => size.height, onFalse: () => size.width })
|
|
242
|
+
const targetInset = Bool.match(y, { onTrue: () => inset.y, onFalse: () => inset.x })
|
|
243
|
+
return progressThrough(Arr.map(offset, (stop) => positionOf(stop, containerLength, targetLength, targetInset)), current)
|
|
244
|
+
}
|
|
245
|
+
return () => {
|
|
246
|
+
const x = Math.abs(element.scrollLeft)
|
|
247
|
+
const y = Math.abs(element.scrollTop)
|
|
248
|
+
const xLength = element.scrollWidth - element.clientWidth
|
|
249
|
+
const yLength = element.scrollHeight - element.clientHeight
|
|
250
|
+
const resolvesOn = (on: "x" | "y"): boolean => Bool.and(resolves, axis === on)
|
|
251
|
+
return {
|
|
252
|
+
x: { current: x, length: xLength, progress: Bool.match(resolvesOn("x"), { onTrue: () => onAxis(x), onFalse: () => plainProgress(x, xLength) }) },
|
|
253
|
+
y: { current: y, length: yLength, progress: Bool.match(resolvesOn("y"), { onTrue: () => onAxis(y), onFalse: () => plainProgress(y, yLength) }) },
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Whether two scrolls read the same: an unchanged one isn't set, so its readers hear nothing. */
|
|
259
|
+
const sameScroll = (a: Probe.Scroll, b: Probe.Scroll): boolean =>
|
|
260
|
+
Arr.every(
|
|
261
|
+
[
|
|
262
|
+
a.x.current === b.x.current,
|
|
263
|
+
a.y.current === b.y.current,
|
|
264
|
+
a.x.length === b.x.length,
|
|
265
|
+
a.y.length === b.y.length,
|
|
266
|
+
a.x.progress === b.x.progress,
|
|
267
|
+
a.y.progress === b.y.progress,
|
|
268
|
+
],
|
|
269
|
+
Boolean,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
/** The page's scroller: `document.scrollingElement`, else the root element. */
|
|
273
|
+
const pageOf = (): Element => Option.getOrElse(Option.fromNullOr(document.scrollingElement), () => document.documentElement)
|
|
274
|
+
|
|
275
|
+
/** The DOM's `Probe.scroll`, its Atoms set through `registry`: its containers held in the current Scope. */
|
|
276
|
+
export const makeScroll = (
|
|
277
|
+
registry: AtomRegistry.AtomRegistry,
|
|
278
|
+
): Effect.Effect<(options?: Probe.ScrollOptions) => Effect.Effect<Atom.Atom<Probe.Scroll>, never, Scope.Scope>, never, Scope.Scope> =>
|
|
279
|
+
Effect.map(
|
|
280
|
+
// Each container's listeners while a reader holds them, the last one out removing them (#618, M7:
|
|
281
|
+
// Effect's `RcMap` where a count was kept by hand). An element is keyed by reference: Effect's
|
|
282
|
+
// structural `Hash` would walk a DOM node.
|
|
283
|
+
RcMap.make({ lookup: (element: Element) => Effect.acquireRelease(Effect.sync(() => open(element)), (container) => Effect.sync(container.remove)) }),
|
|
284
|
+
(containers) => readerOf(registry, containers),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
/** A container's listener set, opened for its first reader. */
|
|
288
|
+
const open = (element: Element): Container => {
|
|
289
|
+
const readers = new Set<() => void>()
|
|
290
|
+
const readAll = (): void => Iter.forEach(readers, (read) => read())
|
|
291
|
+
// The page's scroll is the window's event, and its range changes with the window.
|
|
292
|
+
const page = Bool.or(element === document.scrollingElement, element === document.documentElement)
|
|
293
|
+
const scrolls: EventTarget = Bool.match(page, { onTrue: (): EventTarget => window, onFalse: () => element })
|
|
294
|
+
scrolls.addEventListener("scroll", readAll, { passive: true })
|
|
295
|
+
window.addEventListener("resize", readAll)
|
|
296
|
+
// Sizes (#239): a container's own box, its content's, and its readers' targets'. One observer,
|
|
297
|
+
// each box counted by the reasons it's watched for (a target can be a child too).
|
|
298
|
+
const sizes = new ResizeObserver(readAll)
|
|
299
|
+
const watched = counted(sizes)
|
|
300
|
+
// Its own resize changes a container's range (the page's is the window's), and so does its
|
|
301
|
+
// content's: its direct children, kept as they come and go. A removed child resizes nothing still
|
|
302
|
+
// watched, and changes the range all the same.
|
|
303
|
+
const stopChildren = Bool.match(page, {
|
|
304
|
+
onTrue: () => watchPage(watched, readAll),
|
|
305
|
+
onFalse: () => {
|
|
306
|
+
watched.observe(element)
|
|
307
|
+
return watchChildren(element, watched, readAll)
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
const container: Container = {
|
|
311
|
+
readers,
|
|
312
|
+
watch: (target) => {
|
|
313
|
+
watched.observe(target)
|
|
314
|
+
return () => watched.unobserve(target)
|
|
315
|
+
},
|
|
316
|
+
remove: () => {
|
|
317
|
+
scrolls.removeEventListener("scroll", readAll)
|
|
318
|
+
window.removeEventListener("resize", readAll)
|
|
319
|
+
sizes.disconnect()
|
|
320
|
+
stopChildren()
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
return container
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** A reader of a container's scroll: the container's listeners held from `containers` while its Scope is open. */
|
|
327
|
+
const readerOf = (registry: AtomRegistry.AtomRegistry, containers: RcMap.RcMap<Element, Container>) =>
|
|
328
|
+
Effect.fnUntraced(function* (options: Probe.ScrollOptions = {}) {
|
|
329
|
+
const element = UndefinedOr.match(options.container as Element | undefined, { onUndefined: pageOf, onDefined: (given) => given })
|
|
330
|
+
const container = yield* RcMap.get(containers, Equal.byReferenceUnsafe(element))
|
|
331
|
+
const measure = measureOf(element, options)
|
|
332
|
+
let last = measure()
|
|
333
|
+
// Mounted while read, not kept alive: a kept-alive Atom's node never leaves the registry.
|
|
334
|
+
const atom = Atom.make(last)
|
|
335
|
+
const unmount = registry.mount(atom)
|
|
336
|
+
const read = (): void => {
|
|
337
|
+
const next = measure()
|
|
338
|
+
if (sameScroll(last, next)) return
|
|
339
|
+
last = next
|
|
340
|
+
registry.set(atom, next)
|
|
341
|
+
}
|
|
342
|
+
yield* Effect.acquireRelease(
|
|
343
|
+
Effect.sync(() => container.readers.add(read)),
|
|
344
|
+
() =>
|
|
345
|
+
Effect.sync(() => {
|
|
346
|
+
container.readers.delete(read)
|
|
347
|
+
unmount()
|
|
348
|
+
}),
|
|
349
|
+
)
|
|
350
|
+
// A target's own size moves its offsets (#239): watched while this reader reads.
|
|
351
|
+
yield* Option.match(Option.fromUndefinedOr(options.target as Element | undefined), {
|
|
352
|
+
onNone: () => Effect.void,
|
|
353
|
+
onSome: (target) => Effect.acquireRelease(Effect.sync(() => container.watch(target)), (unwatch) => Effect.sync(unwatch)),
|
|
354
|
+
})
|
|
355
|
+
return atom
|
|
356
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which scrollers move an element on screen (ADR-0012), apart from the Measure pass (#548): the gesture
|
|
3
|
+
* input and the reveals read `scrollersOf` on every page, so it sits in a module of its own, and the
|
|
4
|
+
* Projection system's reads, in `measure.ts`, ship only where an app lays out (ADR-0067,
|
|
5
|
+
* *Amendment (#548)*). Per node in the Measure pass: a `for` up the ancestors (ADR-0025, dom's
|
|
6
|
+
* per-frame files).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The overflow values that never scroll: anything else can (`hidden` too, from code). */
|
|
10
|
+
const UNSCROLLED: ReadonlySet<string> = new Set(["visible", "clip"])
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the scroll fold and a scroll feed need from an element's style, from one
|
|
14
|
+
* `getComputedStyle` read (ADR-0012, *Amendment (Reorder)*): whether it can scroll on
|
|
15
|
+
* either axis (`overflow-x` or `overflow-y` neither `visible` nor `clip`; Motion's
|
|
16
|
+
* auto-scroll asks for `auto | scroll`, but a `hidden` container can still be scrolled
|
|
17
|
+
* from code), and whether it is `position: fixed` (its subtree is viewport-relative,
|
|
18
|
+
* so no scroll outside it moves anything inside).
|
|
19
|
+
*/
|
|
20
|
+
export interface ScrollStyle {
|
|
21
|
+
readonly scrollable: boolean
|
|
22
|
+
readonly fixed: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** {@link ScrollStyle} from an element's computed style. */
|
|
26
|
+
export const scrollStyleFrom = (style: CSSStyleDeclaration): ScrollStyle => ({
|
|
27
|
+
scrollable: !UNSCROLLED.has(style.overflowX) || !UNSCROLLED.has(style.overflowY),
|
|
28
|
+
fixed: style.position === "fixed",
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/** {@link ScrollStyle} of `el`, uncached: one `getComputedStyle` per call. */
|
|
32
|
+
export const readScrollStyle = (el: Element): ScrollStyle => scrollStyleFrom(getComputedStyle(el))
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The scrollers whose scroll moves `el` on screen: its ancestors that can scroll,
|
|
36
|
+
* nearest first, up to the first `position: fixed` one (included if it scrolls), and
|
|
37
|
+
* whether the page's scroll does (no fixed element on the way, `el` included). The
|
|
38
|
+
* walk stops before `body` and `documentElement`: the page's own scroll is the
|
|
39
|
+
* window's.
|
|
40
|
+
*/
|
|
41
|
+
export const scrollersOf = (el: Element): { readonly ancestors: Array<Element>; readonly page: boolean } => {
|
|
42
|
+
const ancestors: Array<Element> = []
|
|
43
|
+
if (readScrollStyle(el).fixed) return { ancestors, page: false }
|
|
44
|
+
const doc = el.ownerDocument
|
|
45
|
+
for (let cur = el.parentElement; cur !== null && cur !== doc.body && cur !== doc.documentElement; cur = cur.parentElement) {
|
|
46
|
+
const style = readScrollStyle(cur)
|
|
47
|
+
if (style.scrollable) ancestors.push(cur)
|
|
48
|
+
if (style.fixed) return { ancestors, page: false }
|
|
49
|
+
}
|
|
50
|
+
return { ancestors, page: true }
|
|
51
|
+
}
|