@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,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM's **Reorder group** controller (ADR-0020): the framework-free half of
|
|
3
|
+
* `<Reorder>`, shared by every DOM binding. A binding adds only its reactivity, its
|
|
4
|
+
* lifecycle and its elements.
|
|
5
|
+
*
|
|
6
|
+
* It owns core's `Reorder.Group` (the **Crossover** and the new order), reads the
|
|
7
|
+
* group element's `direction` at each drag start, builds the group's **Auto-scroll**,
|
|
8
|
+
* and routes each item's drag samples: the start opens the group's gesture, each move
|
|
9
|
+
* runs the Crossover and Auto-scroll, the end (or the item's disposal mid-gesture)
|
|
10
|
+
* closes both. The items' `touch-action` needs the axis before a touch starts, so after
|
|
11
|
+
* every Measure pass the controller detects the axis from the items' boxes and reports
|
|
12
|
+
* a change through `onAxis`; while the app sets the axis it detects nothing. Motion
|
|
13
|
+
* detects on every item's every measurement (Group.tsx:121-133 at 16c6122). When the app
|
|
14
|
+
* clears its axis, no Measure pass comes with that: a binding reads `detect()`, the axis
|
|
15
|
+
* from the boxes the items have now, in the computation that derives its axis, so whatever
|
|
16
|
+
* reads the axis follows in the same update (#257's audit: an effect that detected and
|
|
17
|
+
* reported it updated them a step late).
|
|
18
|
+
*
|
|
19
|
+
* It also runs each item's **Keyboard reorder** (ADR-0037), on by default: a key from the item
|
|
20
|
+
* itself that the page didn't take (Space or Enter lifts and drops, Escape cancels, the arrows,
|
|
21
|
+
* Home and End move the lifted item) goes to core's `Reorder` model, its default prevented; each
|
|
22
|
+
* step is an **Announcement** (a held arrow's thinned to the last), scrolls the item's new place
|
|
23
|
+
* into view after the Measure pass that lays it out, and keeps focus on the item; the lifted node is
|
|
24
|
+
* raised as a dragged one is, and the binding turns its `whileDrag` on. Focus leaving the item, a
|
|
25
|
+
* press on it, or the app changing `values` drops it where it is; its unmount ends the lift unsaid.
|
|
26
|
+
*
|
|
27
|
+
* Each **Reorder hold** (#348, ADR-0020 *Amendment (#348)*), an item in a user's hand from its drag's start or
|
|
28
|
+
* its lift, is told at its start and, once, at its end, whatever ends it: after `onReorderEnd`, when
|
|
29
|
+
* it moved. One at a time: a hold's start ends the open one first, and that item's later release
|
|
30
|
+
* tells nothing more.
|
|
31
|
+
* `move` announces too. Nothing is announced without an `announce`.
|
|
32
|
+
*
|
|
33
|
+
* Observability: `wardic.reorder:` debug logs through the runtime, annotated: the
|
|
34
|
+
* group's and Auto-scroll's events, and `axis` when the detected axis changes (cold
|
|
35
|
+
* path: never per sample), plus one warning per controller for a remount (below).
|
|
36
|
+
*
|
|
37
|
+
* Tier 2 (ADR-0008): for DOM binding authors.
|
|
38
|
+
*/
|
|
39
|
+
import * as AutoScroll from "@wardic/core/AutoScroll"
|
|
40
|
+
import type * as Drag from "@wardic/core/Drag"
|
|
41
|
+
import type * as InputSource from "@wardic/core/InputSource"
|
|
42
|
+
import * as Reorder from "@wardic/core/Reorder"
|
|
43
|
+
import * as Bool from "effect/Boolean"
|
|
44
|
+
import * as Duration from "effect/Duration"
|
|
45
|
+
import * as Effect from "effect/Effect"
|
|
46
|
+
import * as FiberHandle from "effect/FiberHandle"
|
|
47
|
+
import { constTrue, constVoid, identity } from "effect/Function"
|
|
48
|
+
import * as Match from "effect/Match"
|
|
49
|
+
import * as MutableRef from "effect/MutableRef"
|
|
50
|
+
import * as Option from "effect/Option"
|
|
51
|
+
import * as Predicate from "effect/Predicate"
|
|
52
|
+
import * as Scope from "effect/Scope"
|
|
53
|
+
import * as Str from "effect/String"
|
|
54
|
+
import * as UndefinedOr from "effect/UndefinedOr"
|
|
55
|
+
import { documentOf, prepareAnnouncer } from "./announcer.ts"
|
|
56
|
+
import { type AutoScroller, makeAutoScroller } from "./auto-scroll.ts"
|
|
57
|
+
import type { ProjectionNodeHandle, ProjectionSystem } from "./projection.ts"
|
|
58
|
+
import { debugOf } from "./report.ts"
|
|
59
|
+
import { revealLayoutOf } from "./reveal.ts"
|
|
60
|
+
import type { Runner } from "./runner.ts"
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The group element's style: no scroll anchoring, so the browser doesn't shift the
|
|
64
|
+
* page as items move (Motion's Group.tsx:187-195 at 16c6122). The user's style goes
|
|
65
|
+
* over it.
|
|
66
|
+
*/
|
|
67
|
+
export const REORDER_GROUP_STYLE = Object.freeze({ "overflow-anchor": "none" } as const)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What {@link makeReorderController} runs on: the runner it logs through, the projection system the
|
|
71
|
+
* items are nodes of, and the Environment's `Announcer`. `ReorderController.make` takes them from
|
|
72
|
+
* its context (#132's PR 3, ADR-0041).
|
|
73
|
+
*/
|
|
74
|
+
export interface ReorderControllerWiring {
|
|
75
|
+
/** The runner the controller logs through and waits in. */
|
|
76
|
+
readonly runtime: Runner<never>
|
|
77
|
+
/** The projection system the items are nodes of. */
|
|
78
|
+
readonly projection: Omit<ProjectionSystem, "dispose" | "onPassEnd">
|
|
79
|
+
/** Speak an **Announcement**: the Environment's `Announcer`. Left out, nothing is announced. */
|
|
80
|
+
readonly announce?: ((message: string) => void) | undefined
|
|
81
|
+
/** The engine's Measure-pass ends: its `PassEnds` service (internal/pass-ends.ts). */
|
|
82
|
+
readonly onPassEnd: (listener: () => void) => () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A user's reorder that changed the order, at its end: a pointer drag released, or a keyboard lift
|
|
87
|
+
* dropped, with the item at another index than at its start. What an app saves: the whole order, or the
|
|
88
|
+
* one item that moved and where.
|
|
89
|
+
*/
|
|
90
|
+
// #257 item 3.
|
|
91
|
+
export interface ReorderEnd<V> {
|
|
92
|
+
/** The order at the end. */
|
|
93
|
+
readonly values: Array<V>
|
|
94
|
+
/** The item the user moved: the one dragged, or lifted. */
|
|
95
|
+
readonly value: V
|
|
96
|
+
/** Its index at the gesture's start. */
|
|
97
|
+
readonly from: number
|
|
98
|
+
/** Its index at the end. */
|
|
99
|
+
readonly to: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A **Reorder hold**'s start: the item taken in hand, by its drag's start or a keyboard lift, and its index then. */
|
|
103
|
+
// #348.
|
|
104
|
+
export interface ReorderHoldStart<V> {
|
|
105
|
+
/** The item taken in hand. */
|
|
106
|
+
readonly value: V
|
|
107
|
+
/** Its index at the start. */
|
|
108
|
+
readonly from: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How a **Reorder hold** ended: `"drop"`, released or dropped where it is (a pointer's release or
|
|
113
|
+
* cancel, a drop key, focus leaving, a press, the app's `values`, another hold); `"cancel"`, Escape, the
|
|
114
|
+
* order put back; `"unmount"`, its item went away.
|
|
115
|
+
*/
|
|
116
|
+
// #348.
|
|
117
|
+
export type ReorderHoldHow = "drop" | "cancel" | "unmount"
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A **Reorder hold**'s end, whatever ended it, once: the order then, the item, its index at the start
|
|
121
|
+
* and now (`-1` once it's out of the values), and how. After `onReorderEnd`, in the same task, when the
|
|
122
|
+
* order changed.
|
|
123
|
+
*/
|
|
124
|
+
// #348.
|
|
125
|
+
export interface ReorderHoldEnd<V> extends ReorderEnd<V> {
|
|
126
|
+
/** How it ended. */
|
|
127
|
+
readonly how: ReorderHoldHow
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What a binding hands its group's controller. */
|
|
131
|
+
export interface ReorderControllerOptions<V> {
|
|
132
|
+
/** The app's values, in order. */
|
|
133
|
+
readonly values: () => ReadonlyArray<V>
|
|
134
|
+
/** Receives a new order; the app sets `values` from it. */
|
|
135
|
+
readonly onReorder: (values: Array<V>) => void
|
|
136
|
+
/**
|
|
137
|
+
* Hears a user's reorder that changed the order, at its end: once a gesture, not at each step, as
|
|
138
|
+
* `onReorder` is. Not a keyboard cancel, which puts the order back, nor `move`.
|
|
139
|
+
*/
|
|
140
|
+
// #257 item 3.
|
|
141
|
+
readonly onReorderEnd?: ((end: ReorderEnd<V>) => void) | undefined
|
|
142
|
+
/** Hears each **Reorder hold**'s start: a drag of an item past its threshold, or a keyboard lift. */
|
|
143
|
+
// #348.
|
|
144
|
+
readonly onHoldStart?: ((start: ReorderHoldStart<V>) => void) | undefined
|
|
145
|
+
/** Hears each **Reorder hold**'s end, once, whatever ended it; after `onReorderEnd` when it moved. */
|
|
146
|
+
// #348.
|
|
147
|
+
readonly onHoldEnd?: ((end: ReorderHoldEnd<V>) => void) | undefined
|
|
148
|
+
/** The app's axis, overriding detection; `None` to detect. */
|
|
149
|
+
readonly axis?: (() => Option.Option<Reorder.Axis>) | undefined
|
|
150
|
+
/** Called when the detected axis changes (after a Measure pass, or {@link ReorderController.resolve}). */
|
|
151
|
+
readonly onAxis?: ((axis: Reorder.Axis) => void) | undefined
|
|
152
|
+
/**
|
|
153
|
+
* How the binding keys its items, for the warning logged once per controller when an
|
|
154
|
+
* item registers with a value an item of this group unregistered in the same task
|
|
155
|
+
* (the signature of a list keyed by index): "key items with `keyedWith`". Solid's is
|
|
156
|
+
* `"<For>"`; by default "their value".
|
|
157
|
+
*/
|
|
158
|
+
readonly keyedWith?: string | undefined
|
|
159
|
+
/** Whether the items reorder by keyboard, read at each key; `false` turns it off. On when left out. */
|
|
160
|
+
// ADR-0037.
|
|
161
|
+
readonly keyboard?: (() => boolean) | undefined
|
|
162
|
+
/** The group's announcements over the defaults, read at each announcement. */
|
|
163
|
+
// ADR-0037.
|
|
164
|
+
readonly announcements?: (() => Option.Option<Partial<Reorder.Announcements<V>>>) | undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One item of a {@link ReorderController}: its binding feeds it the item's handle, drag samples, keys, focus and presses. */
|
|
168
|
+
export interface ReorderItemDriver {
|
|
169
|
+
/** Register the item's projection handle with the group; returns the unregister. */
|
|
170
|
+
readonly register: (handle: ProjectionNodeHandle) => () => void
|
|
171
|
+
/** The axis the item's drag takes, read at each gesture's start. */
|
|
172
|
+
readonly dragAxis: () => Reorder.Axis
|
|
173
|
+
/** The item's drag samples, as the drag delivers them. */
|
|
174
|
+
readonly onSample: (sample: Drag.Sample) => void
|
|
175
|
+
/** A key at the item or inside it, inside its dispatch: a keyboard reorder's. */
|
|
176
|
+
// ADR-0037.
|
|
177
|
+
readonly onKey: (event: InputSource.KeyboardLikeEvent) => void
|
|
178
|
+
/** Focus left the item: a lift drops where it is, unless focus is back on the item. */
|
|
179
|
+
// ADR-0037.
|
|
180
|
+
readonly onBlur: () => void
|
|
181
|
+
/** A press on the item: a lift drops where it is, before any drag. */
|
|
182
|
+
// ADR-0037.
|
|
183
|
+
readonly onPress: () => void
|
|
184
|
+
/** The item is going away: end a gesture or a lift still in progress. */
|
|
185
|
+
readonly dispose: () => void
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** How long a held arrow's announcements wait for the next repeat: only the last is spoken (ADR-0037, Q4). */
|
|
189
|
+
export const REPEAT_ANNOUNCE: Duration.Duration = Duration.millis(150)
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A Reorder item's default instructions on the web: core's keys, and focus mode. A
|
|
193
|
+
* screen reader in browse mode (NVDA's, JAWS's) keeps Space and the arrows for itself until its user
|
|
194
|
+
* switches to focus mode, and it doesn't switch by itself on a list item.
|
|
195
|
+
*/
|
|
196
|
+
// ADR-0037: the NVDA pass.
|
|
197
|
+
export const DEFAULT_REORDER_INSTRUCTIONS = `${Reorder.DEFAULT_INSTRUCTIONS} Some screen readers need focus mode for these keys.`
|
|
198
|
+
|
|
199
|
+
/** How an item is spoken: its `aria-label`, else its text with its whitespace collapsed, else its value. */
|
|
200
|
+
const labelOf = (element: Option.Option<Element>, value: unknown): string =>
|
|
201
|
+
Option.getOrElse(
|
|
202
|
+
Option.orElse(
|
|
203
|
+
Option.flatMap(element, (el) => nonEmpty(Option.map(Option.fromNullOr(el.getAttribute("aria-label")), Str.trim))),
|
|
204
|
+
() => Option.flatMap(element, (el) => nonEmpty(Option.map(Option.fromNullOr(el.textContent), (text) => text.replace(/\s+/g, " ").trim()))),
|
|
205
|
+
),
|
|
206
|
+
() => String(value),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
/** A text, `None` when it's empty. */
|
|
210
|
+
const nonEmpty = (text: Option.Option<string>): Option.Option<string> => Option.filter(text, Str.isNonEmpty)
|
|
211
|
+
|
|
212
|
+
/** A **Reorder group** on the DOM. */
|
|
213
|
+
// ADR-0020.
|
|
214
|
+
export interface ReorderController<V> {
|
|
215
|
+
/** Core's Reorder group (`Reorder.move(group, value, toIndex)` reorders in code). */
|
|
216
|
+
readonly group: Reorder.Group<V>
|
|
217
|
+
/** The app's axis, else the last detected one (`"y"` before the first). */
|
|
218
|
+
readonly axis: () => Reorder.Axis
|
|
219
|
+
/** The group's element: its `direction`, and the scroller Auto-scroll looks for from it. */
|
|
220
|
+
readonly attach: (element: Element) => void
|
|
221
|
+
/**
|
|
222
|
+
* The driver for the item with `value`; `element` is its element (for Auto-scroll, its label, its
|
|
223
|
+
* focus and its reveal), and `onLift` hears its keyboard lift start and end (the binding's `whileDrag`).
|
|
224
|
+
*/
|
|
225
|
+
readonly item: (value: V, element: () => Option.Option<Element>, onLift?: (lifted: boolean) => void) => ReorderItemDriver
|
|
226
|
+
/** Move `value` to `toIndex` in code (`Reorder.move`), and announce it. */
|
|
227
|
+
// ADR-0037.
|
|
228
|
+
readonly move: (value: V, toIndex: number) => void
|
|
229
|
+
/**
|
|
230
|
+
* The axis now: the app's, else detected from the items' boxes as they are, reporting nothing. A
|
|
231
|
+
* binding derives its axis with it when the app's is cleared.
|
|
232
|
+
*/
|
|
233
|
+
// #257's audit.
|
|
234
|
+
readonly detect: () => Reorder.Axis
|
|
235
|
+
/** The app's values changed: re-measure, so the items animate to their slots. */
|
|
236
|
+
readonly valuesChanged: () => void
|
|
237
|
+
/** Stops listening to measures and ends the auto-scroll. The Scope calls it. */
|
|
238
|
+
readonly dispose: () => void
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Build a {@link ReorderController}: a **Reorder group** over `wiring.projection`'s nodes. */
|
|
242
|
+
export const makeReorderController = <V>(wiring: ReorderControllerWiring, options: ReorderControllerOptions<V>): ReorderController<V> => {
|
|
243
|
+
const { runtime, projection, announce } = wiring
|
|
244
|
+
const debug = debugOf(runtime, "wardic.reorder")
|
|
245
|
+
// Speak an Announcement, or nothing without an `announce`.
|
|
246
|
+
const speak = UndefinedOr.match(announce, { onUndefined: () => constVoid, onDefined: identity })
|
|
247
|
+
const keyboardOn = (): boolean => UndefinedOr.match(options.keyboard, { onUndefined: constTrue, onDefined: (on) => on() })
|
|
248
|
+
// The group's element and its Auto-scroll, once it's attached.
|
|
249
|
+
const element = MutableRef.make(Option.none<Element>())
|
|
250
|
+
const autoScroller = MutableRef.make(Option.none<AutoScroller>())
|
|
251
|
+
const resetAutoScroll = (): void => {
|
|
252
|
+
const scroller = MutableRef.get(autoScroller)
|
|
253
|
+
if (Option.isSome(scroller)) scroller.value.reset()
|
|
254
|
+
}
|
|
255
|
+
// The order the group last asked for: a `values` change to anything else is the app's own (ADR-0037).
|
|
256
|
+
let asked: ReadonlyArray<V> | undefined
|
|
257
|
+
/** The app's axis, if it sets one. */
|
|
258
|
+
const appAxis = (): Option.Option<Reorder.Axis> => Option.flatMap(Option.fromUndefinedOr(options.axis), (axis) => axis())
|
|
259
|
+
const group = Reorder.make<V>({
|
|
260
|
+
values: options.values,
|
|
261
|
+
onReorder: (values) => {
|
|
262
|
+
asked = values
|
|
263
|
+
options.onReorder(values)
|
|
264
|
+
},
|
|
265
|
+
axis: () => appAxis(),
|
|
266
|
+
// Read once per drag start (Motion reads it on every sample, Group.tsx:142-147 at 16c6122).
|
|
267
|
+
direction: () =>
|
|
268
|
+
Bool.match(Option.exists(MutableRef.get(element), (el) => getComputedStyle(el).direction === "rtl"), {
|
|
269
|
+
onTrue: () => "rtl" as const,
|
|
270
|
+
onFalse: () => "ltr" as const,
|
|
271
|
+
}),
|
|
272
|
+
debug,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
let detected: Reorder.Axis = "y"
|
|
276
|
+
let reported: Reorder.Axis | undefined
|
|
277
|
+
const resolve = (): void => {
|
|
278
|
+
if (Option.isSome(appAxis())) return
|
|
279
|
+
detected = Reorder.resolveAxis(group)
|
|
280
|
+
if (detected === reported) return
|
|
281
|
+
reported = detected
|
|
282
|
+
debug("axis", { axis: detected })
|
|
283
|
+
if (Predicate.isNotUndefined(options.onAxis)) options.onAxis(detected)
|
|
284
|
+
}
|
|
285
|
+
// --- The keyboard reorder (ADR-0037) ---------------------------------------
|
|
286
|
+
/** The lifted item's element, its handle and its `onLift`, by value, while it's registered. */
|
|
287
|
+
interface Keyed {
|
|
288
|
+
readonly element: () => Option.Option<Element>
|
|
289
|
+
handle: Option.Option<ProjectionNodeHandle>
|
|
290
|
+
readonly onLift: (lifted: boolean) => void
|
|
291
|
+
}
|
|
292
|
+
const keyed = new Map<V, Keyed>()
|
|
293
|
+
const elementOf = (value: V): Option.Option<Element> => Option.flatMap(Option.fromUndefinedOr(keyed.get(value)), (k) => k.element())
|
|
294
|
+
// The item whose new place the next Measure pass lays out, to scroll into view then.
|
|
295
|
+
const reveal = MutableRef.make(Option.none<V>())
|
|
296
|
+
// A held arrow's latest announcement, waiting for the next repeat (ADR-0025, *Amendment (#69)*): a
|
|
297
|
+
// fiber in the Scope the controller was built in, which closes it after `dispose`.
|
|
298
|
+
const pending = runtime.runSync(FiberHandle.make<void, never>().pipe(Scope.provide(runtime.scope)))
|
|
299
|
+
|
|
300
|
+
const announceNow = (message: string): void => {
|
|
301
|
+
runtime.runFork(FiberHandle.clear(pending))
|
|
302
|
+
speak(message)
|
|
303
|
+
}
|
|
304
|
+
/** A held arrow's step: spoken once the repeats stop for {@link REPEAT_ANNOUNCE}, only the last. */
|
|
305
|
+
const announceLater = (message: string): void => {
|
|
306
|
+
FiberHandle.setUnsafe(pending, runtime.runFork(Effect.sleep(REPEAT_ANNOUNCE).pipe(Effect.andThen(Effect.sync(() => speak(message))))))
|
|
307
|
+
}
|
|
308
|
+
const wording = (): Reorder.Announcements<V> => ({
|
|
309
|
+
...(Reorder.defaultAnnouncements as Reorder.Announcements<V>),
|
|
310
|
+
...Option.getOrUndefined(Option.flatMap(Option.fromUndefinedOr(options.announcements), (announcements) => announcements())),
|
|
311
|
+
})
|
|
312
|
+
const say = (kind: keyof Reorder.Announcements<V>, value: V, placement: Reorder.Placement): string =>
|
|
313
|
+
wording()[kind]({ value, label: labelOf(elementOf(value), value), position: placement.position, count: placement.count })
|
|
314
|
+
|
|
315
|
+
/** Raise the lifted item (or lower it) and tell its binding (its `whileDrag`). */
|
|
316
|
+
const setLifted = (value: V, lifted: boolean): void => {
|
|
317
|
+
const k = keyed.get(value)
|
|
318
|
+
if (Predicate.isUndefined(k)) return
|
|
319
|
+
if (Option.isSome(k.handle)) k.handle.value.setLifted(lifted)
|
|
320
|
+
k.onLift(lifted)
|
|
321
|
+
}
|
|
322
|
+
/** `value`'s index in the app's values now. */
|
|
323
|
+
const indexOf = (value: V): number => options.values().indexOf(value)
|
|
324
|
+
/** The end of a user's reorder of `value`, begun at `from`: told when it moved (#257 item 3). */
|
|
325
|
+
const ended = (value: V, from: number): void => {
|
|
326
|
+
const to = indexOf(value)
|
|
327
|
+
if (to === from) return
|
|
328
|
+
UndefinedOr.map(options.onReorderEnd, (tell) => tell({ values: [...options.values()], value, from, to }))
|
|
329
|
+
}
|
|
330
|
+
// --- Holds (#348) ------------------------------------------------------------
|
|
331
|
+
/** The open **Reorder hold**: the item in hand, how it was taken, and its index then. One at a time. */
|
|
332
|
+
let held: Option.Option<{ readonly value: V; readonly from: number; readonly by: "drag" | "lift" }> = Option.none()
|
|
333
|
+
/** Whether `value`'s drag is the open hold: a drag a later hold took over tells nothing more. */
|
|
334
|
+
const holdsDrag = (value: V): boolean => Option.exists(held, (h) => h.by === "drag" && h.value === value)
|
|
335
|
+
const holdStart = (value: V, from: number, by: "drag" | "lift"): void => {
|
|
336
|
+
held = Option.some({ value, from, by })
|
|
337
|
+
UndefinedOr.map(options.onHoldStart, (tell) => tell({ value, from }))
|
|
338
|
+
}
|
|
339
|
+
/** End `value`'s hold, `how`: told once, and only for the open one. */
|
|
340
|
+
const holdEnd = (value: V, how: ReorderHoldHow): void =>
|
|
341
|
+
Option.match(Option.filter(held, (h) => h.value === value), {
|
|
342
|
+
onNone: constVoid,
|
|
343
|
+
onSome: (h) => {
|
|
344
|
+
held = Option.none()
|
|
345
|
+
UndefinedOr.map(options.onHoldEnd, (tell) => tell({ values: [...options.values()], value, from: h.from, to: indexOf(value), how }))
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
/** The lifted item's index at its lift. */
|
|
350
|
+
let liftedFrom = -1
|
|
351
|
+
/** Drop the lifted item where it is and announce it: a drop key, or an interruption. */
|
|
352
|
+
const dropLifted = (): void =>
|
|
353
|
+
Option.match(Reorder.liftedValue(group), {
|
|
354
|
+
onNone: constVoid,
|
|
355
|
+
onSome: (value) => {
|
|
356
|
+
Option.match(Reorder.drop(group, value), { onNone: constVoid, onSome: (placement) => announceNow(say("drop", value, placement)) })
|
|
357
|
+
setLifted(value, false)
|
|
358
|
+
ended(value, liftedFrom)
|
|
359
|
+
holdEnd(value, "drop")
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
/**
|
|
363
|
+
* A drag's start ends the open hold first (#348, one at a time): a lift is dropped where it is,
|
|
364
|
+
* and another item's drag ends, told as if released, its own release telling nothing more.
|
|
365
|
+
*/
|
|
366
|
+
const endHeld = (): void =>
|
|
367
|
+
Option.match(held, {
|
|
368
|
+
onNone: constVoid,
|
|
369
|
+
onSome: (h) => {
|
|
370
|
+
if (h.by === "lift") return dropLifted()
|
|
371
|
+
ended(h.value, h.from)
|
|
372
|
+
holdEnd(h.value, "drop")
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
/** Focus back on the lifted item, if a reorder took it (moving a focused node can blur it), without scrolling. */
|
|
376
|
+
const keepFocus = (value: V): void =>
|
|
377
|
+
Option.match(Option.filter(elementOf(value), (el) => el.ownerDocument.activeElement !== el), {
|
|
378
|
+
onNone: constVoid,
|
|
379
|
+
onSome: (el) => (el as HTMLElement).focus({ preventScroll: true }),
|
|
380
|
+
})
|
|
381
|
+
/** Whether focus is on `value`'s element. */
|
|
382
|
+
const focused = (value: V): boolean => Option.exists(elementOf(value), (el) => el.ownerDocument.activeElement === el)
|
|
383
|
+
|
|
384
|
+
const onKeyOf = (value: V) => (event: InputSource.KeyboardLikeEvent): void => {
|
|
385
|
+
if (event.type !== "down") return
|
|
386
|
+
if (Bool.not(keyboardOn())) return
|
|
387
|
+
// A key the page took, or one from inside the item (a text field), isn't the reorder's.
|
|
388
|
+
if (event.handled) return
|
|
389
|
+
if (Bool.not(event.fromTarget)) return
|
|
390
|
+
// Keys during a pointer drag are left alone.
|
|
391
|
+
if (Reorder.isDragging(group)) return
|
|
392
|
+
const lifted = Option.contains(Reorder.liftedValue(group), value)
|
|
393
|
+
Option.match(Reorder.keyIntent(event.key), {
|
|
394
|
+
onNone: constVoid,
|
|
395
|
+
onSome: (intent) =>
|
|
396
|
+
Match.value(intent).pipe(
|
|
397
|
+
Match.tag("Toggle", () => {
|
|
398
|
+
event.handle()
|
|
399
|
+
if (event.repeat) return
|
|
400
|
+
if (lifted) return dropLifted()
|
|
401
|
+
// Lifting another item drops the one lifted first.
|
|
402
|
+
dropLifted()
|
|
403
|
+
Option.match(Reorder.lift(group, value), {
|
|
404
|
+
onNone: constVoid,
|
|
405
|
+
onSome: (placement) => {
|
|
406
|
+
liftedFrom = indexOf(value)
|
|
407
|
+
// The values the lift began with are the group's own: a binding re-reading them,
|
|
408
|
+
// the same, mid-lift (a memo whose sources changed) isn't the app changing them (#348).
|
|
409
|
+
asked = options.values()
|
|
410
|
+
setLifted(value, true)
|
|
411
|
+
announceNow(say("lift", value, placement))
|
|
412
|
+
holdStart(value, liftedFrom, "lift")
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
}),
|
|
416
|
+
Match.tag("Cancel", () => {
|
|
417
|
+
if (Bool.not(lifted)) return
|
|
418
|
+
event.handle()
|
|
419
|
+
Option.match(Reorder.cancel(group, value), {
|
|
420
|
+
onNone: constVoid,
|
|
421
|
+
onSome: (placement) => {
|
|
422
|
+
setLifted(value, false)
|
|
423
|
+
keepFocus(value)
|
|
424
|
+
if (placement.moved) MutableRef.set(reveal, Option.some(value))
|
|
425
|
+
announceNow(say("cancel", value, placement))
|
|
426
|
+
holdEnd(value, "cancel")
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
}),
|
|
430
|
+
Match.tagsExhaustive({
|
|
431
|
+
Step: (step) => stepLifted(event, value, lifted, step),
|
|
432
|
+
End: (end) => stepLifted(event, value, lifted, end),
|
|
433
|
+
}),
|
|
434
|
+
),
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
/** An arrow, Home or End: the lifted item's step, its announcement (a held arrow's thinned), its reveal. */
|
|
438
|
+
const stepLifted = (event: InputSource.KeyboardLikeEvent, value: V, lifted: boolean, intent: Extract<Reorder.KeyIntent, { readonly _tag: "Step" | "End" }>): void => {
|
|
439
|
+
// Not lifted, the arrows, Home and End keep their own work (scrolling the page).
|
|
440
|
+
if (Bool.not(lifted)) return
|
|
441
|
+
event.handle()
|
|
442
|
+
Option.match(Reorder.moveLifted(group, value, intent), {
|
|
443
|
+
onNone: constVoid,
|
|
444
|
+
onSome: (placement) => {
|
|
445
|
+
// A repeat before the last step's Measure pass moved nothing: nothing to say.
|
|
446
|
+
if (placement.waiting) return
|
|
447
|
+
if (placement.moved) {
|
|
448
|
+
MutableRef.set(reveal, Option.some(value))
|
|
449
|
+
keepFocus(value)
|
|
450
|
+
}
|
|
451
|
+
const message = say("step", value, placement)
|
|
452
|
+
Bool.match(event.repeat, { onTrue: () => announceLater(message), onFalse: () => announceNow(message) })
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Every Measure pass measures every node, so its end is when the boxes are fresh, and when a
|
|
458
|
+
// stepped item's new place is laid out.
|
|
459
|
+
const stopPassEnd = wiring.onPassEnd(() => {
|
|
460
|
+
resolve()
|
|
461
|
+
const stepped = MutableRef.get(reveal)
|
|
462
|
+
MutableRef.set(reveal, Option.none())
|
|
463
|
+
const el = Option.filter(Option.flatMap(stepped, elementOf), (e) => e.isConnected)
|
|
464
|
+
if (Option.isSome(el)) revealLayoutOf(el.value)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const keyedWith = UndefinedOr.match(options.keyedWith, { onUndefined: () => "their value", onDefined: identity })
|
|
468
|
+
// Values whose item unregistered in this task, and whether a remount was warned of.
|
|
469
|
+
const unregisteredThisTask = new Set<V>()
|
|
470
|
+
let remountReported = false
|
|
471
|
+
|
|
472
|
+
const item = (value: V, itemElement: () => Option.Option<Element>, onLift: (lifted: boolean) => void = constVoid): ReorderItemDriver => {
|
|
473
|
+
// The gesture's axis (core's, fixed at its start), for Auto-scroll's axis.
|
|
474
|
+
let gestureAxis: Reorder.Axis = "y"
|
|
475
|
+
let inGesture = false
|
|
476
|
+
// Its index at the drag's start, for its `onReorderEnd` (#257 item 3).
|
|
477
|
+
let draggedFrom = -1
|
|
478
|
+
const k: Keyed = { element: itemElement, handle: Option.none(), onLift }
|
|
479
|
+
keyed.set(value, k)
|
|
480
|
+
const end = (): void => {
|
|
481
|
+
inGesture = false
|
|
482
|
+
Reorder.dragEnd(group, value)
|
|
483
|
+
resetAutoScroll()
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
register: (handle) => {
|
|
487
|
+
if (!remountReported && unregisteredThisTask.has(value)) {
|
|
488
|
+
remountReported = true
|
|
489
|
+
runtime.runFork(
|
|
490
|
+
Effect.logWarning(
|
|
491
|
+
`wardic.reorder: an item remounted with the same value in one task (key items with ${keyedWith})`,
|
|
492
|
+
).pipe(Effect.annotateLogs({ value: String(value) })),
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
const unregister = Reorder.register(group, value, {
|
|
496
|
+
layoutBox: handle.layoutBox,
|
|
497
|
+
dragOffset: handle.dragOffset,
|
|
498
|
+
layoutVersion: handle.layoutVersion,
|
|
499
|
+
})
|
|
500
|
+
k.handle = Option.some(handle)
|
|
501
|
+
keyed.set(value, k)
|
|
502
|
+
return () => {
|
|
503
|
+
if (Option.contains(k.handle, handle)) k.handle = Option.none()
|
|
504
|
+
unregister()
|
|
505
|
+
if (unregisteredThisTask.size === 0) queueMicrotask(() => unregisteredThisTask.clear())
|
|
506
|
+
unregisteredThisTask.add(value)
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
dragAxis: () => Reorder.resolveAxis(group),
|
|
510
|
+
onSample: (sample) => {
|
|
511
|
+
if (sample.phase === "start") {
|
|
512
|
+
endHeld()
|
|
513
|
+
inGesture = true
|
|
514
|
+
draggedFrom = indexOf(value)
|
|
515
|
+
gestureAxis = Reorder.dragStart(group, value)
|
|
516
|
+
holdStart(value, draggedFrom, "drag")
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
if (sample.phase === "move") {
|
|
520
|
+
const velocity = Reorder.dragMove(group, value, sample.velocity)
|
|
521
|
+
const scroller = MutableRef.get(autoScroller)
|
|
522
|
+
if (Option.isSome(scroller)) {
|
|
523
|
+
const on = AutoScroll.axis(gestureAxis, velocity)
|
|
524
|
+
scroller.value.update(sample.point, on, velocity[on], Option.getOrUndefined(k.element()))
|
|
525
|
+
}
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
end()
|
|
529
|
+
// A drag a later hold took over ended then.
|
|
530
|
+
if (Bool.not(holdsDrag(value))) return
|
|
531
|
+
ended(value, draggedFrom)
|
|
532
|
+
holdEnd(value, "drop")
|
|
533
|
+
},
|
|
534
|
+
onKey: onKeyOf(value),
|
|
535
|
+
onBlur: () => {
|
|
536
|
+
if (Bool.not(Option.contains(Reorder.liftedValue(group), value))) return
|
|
537
|
+
// A reorder can blur the node it moves; focus was put back on it, so it's no interruption.
|
|
538
|
+
if (focused(value)) return
|
|
539
|
+
dropLifted()
|
|
540
|
+
},
|
|
541
|
+
onPress: () => {
|
|
542
|
+
if (Option.contains(Reorder.liftedValue(group), value)) dropLifted()
|
|
543
|
+
},
|
|
544
|
+
dispose: () => {
|
|
545
|
+
if (inGesture) end()
|
|
546
|
+
// Unmounted while lifted: the lift ends, unsaid.
|
|
547
|
+
if (Option.contains(Reorder.liftedValue(group), value)) Reorder.drop(group, value)
|
|
548
|
+
holdEnd(value, "unmount")
|
|
549
|
+
if (keyed.get(value) === k) keyed.delete(value)
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
group,
|
|
556
|
+
axis: () => Option.getOrElse(appAxis(), () => detected),
|
|
557
|
+
attach: (el) => {
|
|
558
|
+
MutableRef.set(element, Option.some(el))
|
|
559
|
+
MutableRef.set(autoScroller, Option.some(makeAutoScroller(el, debug)))
|
|
560
|
+
// The live region has to be in the page before the first lift (ADR-0037, Q7).
|
|
561
|
+
if (keyboardOn() && Predicate.isNotUndefined(announce)) prepareAnnouncer(documentOf(el))
|
|
562
|
+
},
|
|
563
|
+
item,
|
|
564
|
+
move: (value, toIndex) =>
|
|
565
|
+
Option.match(Reorder.move(group, value, toIndex), {
|
|
566
|
+
onNone: constVoid,
|
|
567
|
+
onSome: (placement) => {
|
|
568
|
+
if (placement.moved) announceNow(say("move", value, placement))
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
571
|
+
detect: () => Reorder.resolveAxis(group),
|
|
572
|
+
valuesChanged: () => {
|
|
573
|
+
// The app changed `values` itself while an item is lifted: it's dropped at its new place.
|
|
574
|
+
if (Option.isSome(Reorder.liftedValue(group)) && options.values() !== asked) dropLifted()
|
|
575
|
+
projection.notify()
|
|
576
|
+
},
|
|
577
|
+
dispose: () => {
|
|
578
|
+
stopPassEnd()
|
|
579
|
+
resetAutoScroll()
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const noop = (): void => {}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* A {@link ReorderController} for a server render (ADR-0021), where there's no projection
|
|
588
|
+
* system to drive and nothing to reorder: its group has no values, so every operation on it
|
|
589
|
+
* does nothing, the axis is `"y"`, and an item's driver is inert too. A binding's server path hands it to its group, so code
|
|
590
|
+
* that reads `group`, `move` or `controller` at setup works there. Tier 2, for binding
|
|
591
|
+
* authors.
|
|
592
|
+
*/
|
|
593
|
+
export const INERT_REORDER_CONTROLLER: ReorderController<never> = Object.freeze({
|
|
594
|
+
// A group with no values: every operation on it finds nothing to do.
|
|
595
|
+
group: Reorder.make<never>({ values: () => [], onReorder: noop }),
|
|
596
|
+
axis: () => "y" as const,
|
|
597
|
+
attach: noop,
|
|
598
|
+
item: () => ({ register: () => noop, dragAxis: () => "y" as const, onSample: noop, onKey: noop, onBlur: noop, onPress: noop, dispose: noop }),
|
|
599
|
+
move: noop,
|
|
600
|
+
detect: () => "y" as const,
|
|
601
|
+
valuesChanged: noop,
|
|
602
|
+
dispose: noop,
|
|
603
|
+
})
|
|
604
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a binding's failure stops (ADR-0030, decision 4): `reportFailure` logs it at Error and
|
|
3
|
+
* reports it to the Environment's `ErrorReporter`s. Core doesn't report: the Frame loop hands each
|
|
4
|
+
* run's failure to its caller (ADR-0027, decision 7), and a Tier 2 caller may recover. A binding
|
|
5
|
+
* boundary that ends a fiber nobody observes calls this, so a failure there reaches the console
|
|
6
|
+
* and an app's reporters instead of stopping its feature with nothing said.
|
|
7
|
+
*
|
|
8
|
+
* An app installs its reporters on the Environment it hands the provider,
|
|
9
|
+
* `ErrorReporter.layer([…])` merged into `env`; Wardic ships none. With none installed, a report
|
|
10
|
+
* is one `getRef` and a size check, on the failure path only. A reporter built with
|
|
11
|
+
* `ErrorReporter.make` dedupes a cause and its original error, so a failure that crosses two
|
|
12
|
+
* boundaries reports once: pass the cause on as it came, never wrapped.
|
|
13
|
+
*
|
|
14
|
+
* Tier 2 (ADR-0008): for binding authors.
|
|
15
|
+
*/
|
|
16
|
+
import * as Bool from "effect/Boolean"
|
|
17
|
+
import * as Cause from "effect/Cause"
|
|
18
|
+
import * as Effect from "effect/Effect"
|
|
19
|
+
import * as ErrorReporter from "effect/ErrorReporter"
|
|
20
|
+
import * as LogLevel from "effect/LogLevel"
|
|
21
|
+
import * as Predicate from "effect/Predicate"
|
|
22
|
+
import * as References from "effect/References"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `cause` logged at Error as `message`, with the cause, and handed to the Environment's
|
|
26
|
+
* `ErrorReporter`s. An interruption is neither: it's how a run is removed, by a retarget, an unmount or a
|
|
27
|
+
* dispose. Annotate the log by piping `Effect.annotateLogs` onto it.
|
|
28
|
+
*/
|
|
29
|
+
export const reportFailure = (message: string, cause: Cause.Cause<unknown>): Effect.Effect<void> =>
|
|
30
|
+
Bool.match(Cause.hasInterruptsOnly(cause), {
|
|
31
|
+
onTrue: () => Effect.void,
|
|
32
|
+
onFalse: () => Effect.logError(message, cause).pipe(Effect.andThen(ErrorReporter.report(cause))),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a Debug log would be kept, as Effect's `MinimumLogLevel` says. Run it once (a runtime, a
|
|
37
|
+
* build) and keep the answer, not once a log.
|
|
38
|
+
*/
|
|
39
|
+
// #618: the spine reads it alone, so an app that logs nothing else doesn't ship debugOf's log.
|
|
40
|
+
export const debugKept: Effect.Effect<boolean> = /*#__PURE__*/ Effect.map(References.MinimumLogLevel, (level) => Bool.not(LogLevel.isGreaterThan(level, "Debug")))
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A lifecycle debug log for `scope` (`wardic.projection`, say): `log(event, annotations?)` logs
|
|
44
|
+
* `scope: event` at Debug, forked on `runner`, with the annotations. Whether a Debug log would be kept
|
|
45
|
+
* is read once, here ({@link debugKept}): when it wouldn't, `on` is `false` and `log` returns at once,
|
|
46
|
+
* forking nothing. A site that builds its annotations per element checks `on` first.
|
|
47
|
+
*/
|
|
48
|
+
// #618 (M4): each call forked a fiber whatever the level, 0.3-0.5 µs to log nothing (one per layout
|
|
49
|
+
// node's mount and unmount); core's Frame loop reads the level once as this does (frameLoop.ts).
|
|
50
|
+
export const debugOf = (runner: { readonly runSync: <A>(effect: Effect.Effect<A>) => A; readonly runFork: <A>(effect: Effect.Effect<A>) => unknown }, scope: string): DebugLog => {
|
|
51
|
+
const on = runner.runSync(debugKept)
|
|
52
|
+
const log = (event: string, annotations?: Record<string, unknown>): void => {
|
|
53
|
+
if (!on) return
|
|
54
|
+
const logged = Effect.logDebug(`${scope}: ${event}`)
|
|
55
|
+
if (Predicate.isUndefined(annotations)) {
|
|
56
|
+
runner.runFork(logged)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
runner.runFork(Effect.annotateLogs(logged, annotations))
|
|
60
|
+
}
|
|
61
|
+
return Object.assign(log, { on })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** {@link debugOf}'s log: called with an event and its annotations, and `on` when a Debug log is kept. */
|
|
65
|
+
export interface DebugLog {
|
|
66
|
+
(event: string, annotations?: Record<string, unknown>): void
|
|
67
|
+
/** Whether a Debug log would be kept: when it's `false`, a call logs nothing. */
|
|
68
|
+
readonly on: boolean
|
|
69
|
+
}
|