@xsynaptic/sonic-ui 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -2
- package/README.md +5 -3
- package/dist/default/define/region.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/envelope.js +11 -11
- package/dist/default/elements/form-element.js +10 -0
- package/dist/default/elements/readout.js +3 -2
- package/dist/default/elements/region-layer.js +251 -0
- package/dist/default/elements/region.js +157 -0
- package/dist/default/elements/sonic-element.js +26 -4
- package/dist/default/elements/split.js +2 -2
- package/dist/default/elements/toggle.js +2 -2
- package/dist/default/elements/value-element.js +9 -5
- package/dist/default/elements/value-gestures.js +7 -5
- package/dist/default/elements/wave-element.js +12 -3
- package/dist/default/elements/waveform.js +38 -2
- package/dist/default/elements/wavestrip.js +90 -17
- package/dist/default/elements/xy.js +3 -11
- package/dist/default/index.js +2 -1
- package/dist/default/lib/clock.js +1 -0
- package/dist/default/lib/drag-step.js +1 -1
- package/dist/default/lib/marker-band.js +40 -29
- package/dist/default/lib/region-drag.js +48 -0
- package/dist/default/lib/spoken-duration.js +3 -0
- package/dist/dev/define/region.d.ts +2 -0
- package/dist/dev/define/region.js +8 -0
- package/dist/dev/define/region.js.map +1 -0
- package/dist/dev/define.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/envelope.js +11 -11
- package/dist/dev/elements/envelope.js.map +1 -1
- package/dist/dev/elements/form-element.d.ts +1 -0
- package/dist/dev/elements/form-element.js +10 -0
- package/dist/dev/elements/form-element.js.map +1 -1
- package/dist/dev/elements/readout.js +3 -2
- package/dist/dev/elements/readout.js.map +1 -1
- package/dist/dev/elements/region-layer.js +252 -0
- package/dist/dev/elements/region-layer.js.map +1 -0
- package/dist/dev/elements/region.d.ts +29 -0
- package/dist/dev/elements/region.js +165 -0
- package/dist/dev/elements/region.js.map +1 -0
- package/dist/dev/elements/sonic-element.d.ts +1 -0
- package/dist/dev/elements/sonic-element.js +27 -5
- package/dist/dev/elements/sonic-element.js.map +1 -1
- package/dist/dev/elements/split.js +2 -2
- package/dist/dev/elements/split.js.map +1 -1
- package/dist/dev/elements/toggle.js +2 -2
- package/dist/dev/elements/toggle.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +3 -1
- package/dist/dev/elements/value-element.js +9 -5
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/value-gestures.js +7 -5
- package/dist/dev/elements/value-gestures.js.map +1 -1
- package/dist/dev/elements/wave-element.d.ts +7 -1
- package/dist/dev/elements/wave-element.js +12 -3
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +7 -0
- package/dist/dev/elements/waveform.js +38 -2
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +17 -1
- package/dist/dev/elements/wavestrip.js +90 -17
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.js +3 -11
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +3 -2
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/clock.js +1 -0
- package/dist/dev/lib/clock.js.map +1 -1
- package/dist/dev/lib/drag-step.js +1 -1
- package/dist/dev/lib/drag-step.js.map +1 -1
- package/dist/dev/lib/marker-band.d.ts +7 -0
- package/dist/dev/lib/marker-band.js +40 -29
- package/dist/dev/lib/marker-band.js.map +1 -1
- package/dist/dev/lib/region-drag.js +49 -0
- package/dist/dev/lib/region-drag.js.map +1 -0
- package/dist/dev/lib/spoken-duration.js +3 -0
- package/dist/dev/lib/spoken-duration.js.map +1 -1
- package/dist/styles/button.css +42 -15
- package/dist/styles/controls.css +1 -0
- package/dist/styles/dial.css +48 -33
- package/dist/styles/envelope.css +7 -6
- package/dist/styles/led.css +28 -13
- package/dist/styles/material/arc.css +2 -2
- package/dist/styles/material/cap.css +25 -15
- package/dist/styles/material/core.css +53 -1
- package/dist/styles/material/glass.css +12 -2
- package/dist/styles/material/groove.css +3 -3
- package/dist/styles/material/keycap.css +5 -5
- package/dist/styles/material/lens.css +1 -1
- package/dist/styles/material/pane.css +21 -11
- package/dist/styles/material/readout.css +23 -10
- package/dist/styles/material/{bracket.css → reticle.css} +15 -8
- package/dist/styles/material/scale.css +4 -4
- package/dist/styles/material/value.css +12 -3
- package/dist/styles/material/well.css +2 -2
- package/dist/styles/material.css +1 -1
- package/dist/styles/meter.css +24 -11
- package/dist/styles/number.css +2 -1
- package/dist/styles/panel.css +5 -6
- package/dist/styles/region.css +88 -0
- package/dist/styles/ring.css +5 -4
- package/dist/styles/screen.css +7 -5
- package/dist/styles/segmented.css +8 -7
- package/dist/styles/slider.css +40 -32
- package/dist/styles/spectrum.css +9 -1
- package/dist/styles/switch.css +52 -53
- package/dist/styles/toggle.css +33 -38
- package/dist/styles/waveform.css +5 -4
- package/dist/styles/wavestrip.css +15 -12
- package/dist/styles/xy.css +19 -19
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @xsynaptic/sonic-ui
|
|
2
2
|
|
|
3
|
+
## 0.25.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `formatClock` writes a time that is not finite (a duration not yet known, a live stream's) as an empty string, where it wrote `NaN:NaN`.
|
|
8
|
+
|
|
9
|
+
## 0.25.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- - `<sonic-region>`: an editable region, a child of a wave strip, drawn as a bracket and registered by `./define/region` with `region.css`. A drag of its body or a key moves it and keeps its length. It has `start`, `end`, `kind`, `name` (submitting `name.start` and `name.end`) and `disabled`, speaks its start, carries `:state(dragging)`, and fires `input` and `change`, which bubble through the strip.
|
|
14
|
+
- Region surface: tokens `--sonic-region-bracket`, `--sonic-region-fill`, `--sonic-region-bracket-width` and `--sonic-region-bracket-radius`; hook classes `.sonic-wavestrip-regions`, `.sonic-region` and `.sonic-region-bracket`; `data-sonic-clipped` on a region written past a bound.
|
|
15
|
+
- Wave strip: `formatSpokenRegion(start, end)` words what a region speaks, and `preview` draws a time as a scrub's edge without scrubbing to it.
|
|
16
|
+
- Wave strip and waveform: `valueFromPoint(clientX, clientY)` is the inverse of `clientXOf`.
|
|
17
|
+
- Wave strip, breaking: the slider role, the tab stop and the focus glow are on `.sonic-wavestrip-canvas`, so `.sonic-wavestrip:focus-visible` no longer matches (`:focus-within` does).
|
|
18
|
+
- - XY pad and envelope: the arcs round a held puck or handle are the reticle. Breaking: `.sonic-xy-bracket` is `.sonic-xy-reticle`, `.sonic-envelope-bracket` is `.sonic-envelope-reticle`, and `material/bracket.css` is `material/reticle.css`.
|
|
19
|
+
- - Waveform: `clientXOf(seconds)` is the viewport x where a time was last drawn, as on the wave strip, so a layer over the wave follows a paged window and a drag. The x is not clamped to the window.
|
|
20
|
+
- Wave strip and waveform: `markersFromPoint(clientX, clientY)` lists the markers at a point, nearest first.
|
|
21
|
+
- Wave strip: `renderMarker(marker, element)` draws a point marker; the element lasts until the next `markers` or `renderMarker` write.
|
|
22
|
+
- Waveform: a marker outside the bounds paints its line at the bound.
|
|
23
|
+
- Wave strip: `--sonic-wave` is renamed `--sonic-unplayed`, beside `--sonic-buffered` and `--sonic-scrub`. Breaking for a skin or host that sets it.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Older browsers, down to Chrome 111, Safari 16.4 and Firefox 128: the built sheets carry vendor prefixes (`-webkit-mask`, `-webkit-user-select`), shading no longer needs relative colour, a dial's scale and the XY pad's lines no longer need the `cap` unit or `sign()`, and typed entry stays shut without the popover API. With no skin, glass follows `Canvas`: an off-white in light, the engine's dark canvas in dark.
|
|
28
|
+
|
|
29
|
+
## 0.24.1
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Older browsers: controls connect without custom states or `Intl.DurationFormat`, a `<label>` without an id is given one where `ariaLabelledByElements` is missing, the readout and typed entry stay shut without anchor positioning, and the shipped sheets no longer use CSS nesting or `sign()`.
|
|
34
|
+
|
|
3
35
|
## 0.24.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
|
@@ -23,8 +55,8 @@
|
|
|
23
55
|
|
|
24
56
|
- - `--sonic-entry-selection` colours the selected text of a typed entry, which opens with its value selected. Unset, it stays the control's lit colour.
|
|
25
57
|
- `--sonic-slider-groove-hover-delay` is how long a slider's groove waits under a pointer before it thickens to `--sonic-slider-groove-hover-ratio`. A press or a keyboard focus thickens it at once and leaving thins it at once. Unset, there is no wait.
|
|
26
|
-
- `--sonic-readout-depth` scales a readout's cast shadow, inner shine and lighter
|
|
27
|
-
- Behaviour change: a readout no longer takes `--sonic-glass-depth` from a screen around it. Inside a screen flattened with `--sonic-glass-depth: 0` its
|
|
58
|
+
- `--sonic-readout-depth` scales a readout's cast shadow, inner shine and lighter center from 0 to 1. Unset, it follows the relief.
|
|
59
|
+
- Behaviour change: a readout no longer takes `--sonic-glass-depth` from a screen around it. Inside a screen flattened with `--sonic-glass-depth: 0` its center follows the relief again, as its shadow always did; set `--sonic-readout-depth: 0` to flatten it.
|
|
28
60
|
|
|
29
61
|
## 0.21.0
|
|
30
62
|
|
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ import '@xsynaptic/sonic-ui/define';
|
|
|
43
43
|
- The split is the one host with a box: `<sonic-split>` is inline until given a display, and laying it out is yours.
|
|
44
44
|
- Register the classes under their stock tag names (`sonic-dial`, `sonic-split`). The sheets and the split key on them, so a split under another tag finds no members.
|
|
45
45
|
- Name every control with `aria-label` or `aria-labelledby` on the host, or with a `<label>` for a form-associated one.
|
|
46
|
+
- Every control that holds a value the user sets is form-associated, as a native input is, and that is deliberate: given a `name` it submits, a form reset returns it to its attributes, a disabled `<fieldset>` disables it, and a `<label>` names it. With no `name` it submits nothing. A value control submits its number and a segmented control its value; a latching button, and a switch or toggle with two positions, submit their `value` only while on, as a checkbox does; an XY pad and a region submit two fields (`name.x` and `name.y`, `name.start` and `name.end`). A momentary button, the displays, the envelope and the split submit nothing.
|
|
46
47
|
- A boolean attribute is on when present, whatever its value, so `pressed="false"` latches a button. From a framework pass `undefined`, never `false`.
|
|
47
48
|
- A control uses only the children it documents and adds one child of its own. Children it draws are copied, so a copy has none of your listeners; keep them static. Under a hydrating framework, register the definitions once hydration is done (in React, from an effect).
|
|
48
49
|
- Events bubble and carry no detail: read the property when one fires. A control knows nothing about playback, so wire its events to your own state and write the property back.
|
|
@@ -75,11 +76,12 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
|
|
|
75
76
|
| --------------- | ----------------------------------------------- |
|
|
76
77
|
| `button.css` | core, cap, well, keycap |
|
|
77
78
|
| `dial.css` | core, cap, glass, readout, value, arc, scale |
|
|
78
|
-
| `envelope.css` | core, glass, readout,
|
|
79
|
+
| `envelope.css` | core, glass, readout, reticle |
|
|
79
80
|
| `led.css` | core, lens |
|
|
80
81
|
| `meter.css` | core, lens, groove |
|
|
81
82
|
| `number.css` | core, cap, value |
|
|
82
83
|
| `panel.css` | core |
|
|
84
|
+
| `region.css` | core, cap |
|
|
83
85
|
| `ring.css` | core, arc |
|
|
84
86
|
| `screen.css` | core, glass, pane, well |
|
|
85
87
|
| `segmented.css` | core, cap, well, keycap |
|
|
@@ -89,8 +91,8 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
|
|
|
89
91
|
| `toggle.css` | core, cap, well, keycap |
|
|
90
92
|
| `waveform.css` | core, cap, glass, readout, value |
|
|
91
93
|
| `wavestrip.css` | core, cap, glass, readout, value |
|
|
92
|
-
| `xy.css` | core, cap, glass, readout,
|
|
94
|
+
| `xy.css` | core, cap, glass, readout, reticle |
|
|
93
95
|
|
|
94
96
|
An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`. Only a missing `core.css` or control sheet is warned about, so check a control that draws wrong against this table.
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
Tested in current Chrome, Firefox and Safari. Known to work from Chrome 111, Safari 16.4 and Firefox 128; older versions are best effort. No control mirrors for right-to-left. MIT licensed.
|
package/dist/default/define.js
CHANGED
|
@@ -36,7 +36,7 @@ function stageParts(envelope, hook, stages) {
|
|
|
36
36
|
}
|
|
37
37
|
const renderEnvelope = template(`
|
|
38
38
|
<div class="sonic-envelope" aria-hidden="true">
|
|
39
|
-
<div class="sonic-envelope-
|
|
39
|
+
<div class="sonic-envelope-reticle"></div>
|
|
40
40
|
<svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
|
|
41
41
|
<path class="sonic-envelope-fill" />
|
|
42
42
|
<rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
|
|
@@ -123,11 +123,10 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
123
123
|
this.reflect("sustain", id);
|
|
124
124
|
}
|
|
125
125
|
#bindings = /* @__PURE__ */ new Map();
|
|
126
|
-
#envelope = renderEnvelope();
|
|
127
|
-
#bracket = requireChild(this.#envelope, ".sonic-envelope-bracket", HTMLDivElement);
|
|
128
126
|
#claim = new ReadoutClaim(() => {
|
|
129
127
|
this.#renderReadout();
|
|
130
128
|
});
|
|
129
|
+
#envelope = renderEnvelope();
|
|
131
130
|
#curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
|
|
132
131
|
#fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
|
|
133
132
|
#floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
|
|
@@ -138,6 +137,7 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
138
137
|
#isDriving = false;
|
|
139
138
|
#line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
|
|
140
139
|
#readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
|
|
140
|
+
#reticle = requireChild(this.#envelope, ".sonic-envelope-reticle", HTMLDivElement);
|
|
141
141
|
#shape = adsrShape({});
|
|
142
142
|
#waiting = /* @__PURE__ */ new Set();
|
|
143
143
|
#warned = /* @__PURE__ */ new Set();
|
|
@@ -229,15 +229,9 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
229
229
|
at: shape.curveHandles.find((point) => point.stage === stage),
|
|
230
230
|
drives: this.#curveDrives(stage)
|
|
231
231
|
});
|
|
232
|
-
this.#
|
|
232
|
+
this.#drawReticle();
|
|
233
233
|
this.#renderReadout();
|
|
234
234
|
}
|
|
235
|
-
#drawBracket() {
|
|
236
|
-
const part = this.#held;
|
|
237
|
-
if (!part) return;
|
|
238
|
-
for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#bracket.style.setProperty(name, part.style.getPropertyValue(name));
|
|
239
|
-
writeAttribute(this.#bracket, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
|
|
240
|
-
}
|
|
241
235
|
#drawPoint(part, point) {
|
|
242
236
|
const { at, drives } = point;
|
|
243
237
|
writeAttribute(part, "hidden", at ? void 0 : "");
|
|
@@ -248,6 +242,12 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
248
242
|
writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
|
|
249
243
|
writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
|
|
250
244
|
}
|
|
245
|
+
#drawReticle() {
|
|
246
|
+
const part = this.#held;
|
|
247
|
+
if (!part) return;
|
|
248
|
+
for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#reticle.style.setProperty(name, part.style.getPropertyValue(name));
|
|
249
|
+
writeAttribute(this.#reticle, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
|
|
250
|
+
}
|
|
251
251
|
#drives(stage) {
|
|
252
252
|
const x = this.#bindings.get(stage);
|
|
253
253
|
const y = stage === "decay" ? this.#bindings.get("sustain") : void 0;
|
|
@@ -263,7 +263,7 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
263
263
|
const pressed = this.#pressed(event);
|
|
264
264
|
if (!pressed || pressed.drives.length === 0) return void 0;
|
|
265
265
|
this.#held = pressed.part;
|
|
266
|
-
this.#
|
|
266
|
+
this.#drawReticle();
|
|
267
267
|
const box = this.#graph.getBoundingClientRect();
|
|
268
268
|
const travel = {
|
|
269
269
|
x: Math.max(1, box.width * this.#shape.share),
|
|
@@ -47,6 +47,16 @@ var SonicFormElement = class extends SonicElement {
|
|
|
47
47
|
if (!internals || this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby")) return [];
|
|
48
48
|
return [...internals.labels].filter((label) => label instanceof Element);
|
|
49
49
|
}
|
|
50
|
+
writeFormFields(fields, state) {
|
|
51
|
+
const { name } = this;
|
|
52
|
+
if (name === "") {
|
|
53
|
+
this.writeFormValue(null, state);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const data = new FormData();
|
|
57
|
+
for (const [field, value] of fields) data.set(`${name}.${field}`, String(value));
|
|
58
|
+
this.writeFormValue(data, state);
|
|
59
|
+
}
|
|
50
60
|
writeFormValue(value, state) {
|
|
51
61
|
this.internals?.setFormValue(value, `${this.localName}:${state}`);
|
|
52
62
|
}
|
|
@@ -2,6 +2,7 @@ import { requireChild } from "../lib/render.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/elements/readout.ts
|
|
4
4
|
const canPopover = "togglePopover" in HTMLElement.prototype;
|
|
5
|
+
const isUnanchored = !CSS.supports("anchor-name", "--a");
|
|
5
6
|
let readoutCount = 0;
|
|
6
7
|
var Readout = class {
|
|
7
8
|
#anchor;
|
|
@@ -27,9 +28,9 @@ var Readout = class {
|
|
|
27
28
|
this.#toggle(isOpen);
|
|
28
29
|
}
|
|
29
30
|
#toggle(isOpen) {
|
|
30
|
-
if (canPopover && this.#bubble.isConnected) this.#bubble.togglePopover(isOpen);
|
|
31
|
+
if (canPopover && !isUnanchored && this.#bubble.isConnected) this.#bubble.togglePopover(isOpen);
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
//#endregion
|
|
35
|
-
export { Readout };
|
|
36
|
+
export { Readout, isUnanchored };
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { writeAttribute } from "../lib/write-attribute.js";
|
|
2
|
+
import { placeChildren, template } from "../lib/render.js";
|
|
3
|
+
import { capturePointer } from "../lib/pointer-drag.js";
|
|
4
|
+
import { dragThresholdPx } from "../lib/drag-step.js";
|
|
5
|
+
import { focusByPointer } from "../lib/focus-by-pointer.js";
|
|
6
|
+
import { SonicRegion, linkRegion, ownRegions } from "./region.js";
|
|
7
|
+
import { writeKind } from "../lib/marker-band.js";
|
|
8
|
+
import { moveRegionTo, regionAt, regionTravel, startRegionDrag, stepRegionDrag } from "../lib/region-drag.js";
|
|
9
|
+
|
|
10
|
+
//#region src/elements/region-layer.ts
|
|
11
|
+
const renderPart = template(`
|
|
12
|
+
<div class="sonic-region" role="slider" aria-orientation="horizontal">
|
|
13
|
+
<div class="sonic-region-bracket"></div>
|
|
14
|
+
</div>
|
|
15
|
+
`, HTMLDivElement);
|
|
16
|
+
function hasMoved(from, to) {
|
|
17
|
+
return from.start !== to.start || from.end !== to.end;
|
|
18
|
+
}
|
|
19
|
+
function clippedEnds(region, [low, high]) {
|
|
20
|
+
const ends = [...Math.min(region.start, region.end) < low ? ["start"] : [], ...Math.max(region.start, region.end) > high ? ["end"] : []];
|
|
21
|
+
return ends.length > 0 ? ends.join(" ") : void 0;
|
|
22
|
+
}
|
|
23
|
+
function writeProperty(part, name, value) {
|
|
24
|
+
if (part.style.getPropertyValue(name) !== value) part.style.setProperty(name, value);
|
|
25
|
+
}
|
|
26
|
+
var RegionLayer = class {
|
|
27
|
+
#drawn = [];
|
|
28
|
+
#held;
|
|
29
|
+
#host;
|
|
30
|
+
#key;
|
|
31
|
+
#keyScrub;
|
|
32
|
+
constructor(host, signal) {
|
|
33
|
+
const render = () => {
|
|
34
|
+
this.render();
|
|
35
|
+
};
|
|
36
|
+
const observer = new MutationObserver(render);
|
|
37
|
+
this.#host = host;
|
|
38
|
+
this.#bindPointer(signal);
|
|
39
|
+
this.#bindKeys(signal);
|
|
40
|
+
observer.observe(host.element, { childList: true });
|
|
41
|
+
ownRegions(host.element, (region) => {
|
|
42
|
+
this.#redraw(region);
|
|
43
|
+
}, signal);
|
|
44
|
+
signal.addEventListener("abort", () => {
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
this.#release();
|
|
47
|
+
this.#endKeyScrub();
|
|
48
|
+
}, { once: true });
|
|
49
|
+
}
|
|
50
|
+
abandon() {
|
|
51
|
+
this.#release(true);
|
|
52
|
+
}
|
|
53
|
+
follow() {
|
|
54
|
+
if (this.#stripKey() !== this.#key) this.render();
|
|
55
|
+
}
|
|
56
|
+
isHeld() {
|
|
57
|
+
return this.#held !== void 0;
|
|
58
|
+
}
|
|
59
|
+
render() {
|
|
60
|
+
const host = this.#host;
|
|
61
|
+
const drawn = [...host.element.children].filter((child) => child instanceof SonicRegion).map((region) => this.#draw(region));
|
|
62
|
+
this.#drawn = drawn;
|
|
63
|
+
this.#key = this.#stripKey();
|
|
64
|
+
placeChildren(host.layer, drawn.map(({ part }) => part));
|
|
65
|
+
this.#dropGone();
|
|
66
|
+
}
|
|
67
|
+
take(event) {
|
|
68
|
+
const host = this.#host;
|
|
69
|
+
const { startPx, travelPx } = host.axis();
|
|
70
|
+
const mapping = host.mapping();
|
|
71
|
+
const index = regionAt(this.#drawn.map((entry) => {
|
|
72
|
+
if (!this.#isEnabled(entry)) return;
|
|
73
|
+
const { end, start } = entry.link.span();
|
|
74
|
+
return [mapping.proportionOf(start) * travelPx, mapping.proportionOf(end) * travelPx];
|
|
75
|
+
}), event.clientX - startPx);
|
|
76
|
+
const pressed = index === void 0 ? void 0 : this.#drawn[index];
|
|
77
|
+
if (!pressed) return false;
|
|
78
|
+
this.#endKeyScrub();
|
|
79
|
+
const from = pressed.link.span();
|
|
80
|
+
this.#held = {
|
|
81
|
+
drawn: pressed,
|
|
82
|
+
from,
|
|
83
|
+
outside: host.outside(),
|
|
84
|
+
pointerId: event.pointerId,
|
|
85
|
+
press: event,
|
|
86
|
+
state: startRegionDrag({
|
|
87
|
+
from,
|
|
88
|
+
position: event.clientX,
|
|
89
|
+
thresholdPx: dragThresholdPx(event.pointerType),
|
|
90
|
+
widthPx: travelPx
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
pressed.link.hold("pointer");
|
|
94
|
+
capturePointer(host.control, event.pointerId);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
#bindKeys(signal) {
|
|
98
|
+
const { layer } = this.#host;
|
|
99
|
+
layer.addEventListener("keydown", (event) => {
|
|
100
|
+
const entry = this.#drawn.find(({ part }) => part === event.target);
|
|
101
|
+
if (!entry || event.defaultPrevented || this.#held) return;
|
|
102
|
+
if (this.#isEnabled(entry)) this.#keyTo(entry, event);
|
|
103
|
+
}, { signal });
|
|
104
|
+
layer.addEventListener("keyup", (event) => {
|
|
105
|
+
if (event.key === "Meta" || event.key === this.#keyScrub?.key) this.#endKeyScrub();
|
|
106
|
+
}, { signal });
|
|
107
|
+
layer.addEventListener("focusin", () => {
|
|
108
|
+
this.render();
|
|
109
|
+
}, { signal });
|
|
110
|
+
layer.addEventListener("focusout", () => {
|
|
111
|
+
this.#endKeyScrub();
|
|
112
|
+
}, { signal });
|
|
113
|
+
}
|
|
114
|
+
#bindPointer(signal) {
|
|
115
|
+
const whileHeld = (type, handle) => {
|
|
116
|
+
this.#host.control.addEventListener(type, (event) => {
|
|
117
|
+
const held = this.#held;
|
|
118
|
+
if (held && event instanceof PointerEvent && held.pointerId === event.pointerId) handle(held, event);
|
|
119
|
+
}, { signal });
|
|
120
|
+
};
|
|
121
|
+
whileHeld("pointermove", (hold, event) => {
|
|
122
|
+
this.#dragTo(hold, event);
|
|
123
|
+
});
|
|
124
|
+
whileHeld("pointerup", (hold) => {
|
|
125
|
+
const isPress = !hold.state.isEngaged;
|
|
126
|
+
this.#release();
|
|
127
|
+
if (isPress) this.#host.seek(hold.press);
|
|
128
|
+
});
|
|
129
|
+
whileHeld("pointercancel", () => {
|
|
130
|
+
this.#release(true);
|
|
131
|
+
});
|
|
132
|
+
whileHeld("lostpointercapture", () => {
|
|
133
|
+
this.#release();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
#dragTo(hold, event) {
|
|
137
|
+
const wasEngaged = hold.state.isEngaged;
|
|
138
|
+
const { span, state } = stepRegionDrag(this.#host.mapping(), hold.state, {
|
|
139
|
+
isFine: event.shiftKey,
|
|
140
|
+
position: event.clientX
|
|
141
|
+
});
|
|
142
|
+
hold.state = state;
|
|
143
|
+
if (!span) return;
|
|
144
|
+
if (!wasEngaged) focusByPointer(hold.drawn.part);
|
|
145
|
+
hold.drawn.link.input(hold.outside?.(event) === true ? hold.from : span);
|
|
146
|
+
}
|
|
147
|
+
#draw(region) {
|
|
148
|
+
const host = this.#host;
|
|
149
|
+
const known = this.#drawn.find((entry) => entry.region === region);
|
|
150
|
+
const entry = known ?? {
|
|
151
|
+
kind: void 0,
|
|
152
|
+
link: linkRegion(region),
|
|
153
|
+
part: renderPart(),
|
|
154
|
+
region
|
|
155
|
+
};
|
|
156
|
+
const { link, part } = entry;
|
|
157
|
+
const mapping = host.mapping();
|
|
158
|
+
const span = link.span();
|
|
159
|
+
const [low, high] = regionTravel(span, mapping.bounds);
|
|
160
|
+
const canMove = this.#isEnabled(entry);
|
|
161
|
+
writeProperty(part, "--_sonic-region-from", String(mapping.proportionOf(span.start)));
|
|
162
|
+
writeProperty(part, "--_sonic-region-to", String(mapping.proportionOf(span.end)));
|
|
163
|
+
if (!known || entry.kind !== region.kind) {
|
|
164
|
+
entry.kind = region.kind;
|
|
165
|
+
writeKind(part, region.kind, "--_sonic-ink");
|
|
166
|
+
}
|
|
167
|
+
writeAttribute(part, "data-sonic-clipped", clippedEnds(region, mapping.bounds));
|
|
168
|
+
writeAttribute(part, "aria-valuemin", String(low));
|
|
169
|
+
writeAttribute(part, "aria-valuemax", String(high));
|
|
170
|
+
writeAttribute(part, "aria-valuenow", String(span.start));
|
|
171
|
+
writeAttribute(part, "aria-valuetext", host.speak(span));
|
|
172
|
+
writeAttribute(part, "aria-disabled", canMove ? void 0 : "true");
|
|
173
|
+
writeAttribute(part, "tabindex", canMove ? "0" : void 0);
|
|
174
|
+
link.draw(part);
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
#dropGone() {
|
|
178
|
+
const isGone = (entry) => !this.#drawn.includes(entry) || !this.#isEnabled(entry);
|
|
179
|
+
if (this.#held && isGone(this.#held.drawn)) this.#release();
|
|
180
|
+
if (this.#keyScrub && isGone(this.#keyScrub.drawn)) this.#endKeyScrub();
|
|
181
|
+
}
|
|
182
|
+
#endKeyScrub() {
|
|
183
|
+
const scrub = this.#keyScrub;
|
|
184
|
+
if (!scrub) return;
|
|
185
|
+
const { link } = scrub.drawn;
|
|
186
|
+
this.#keyScrub = void 0;
|
|
187
|
+
link.hold(void 0);
|
|
188
|
+
if (hasMoved(scrub.from, link.span())) link.change();
|
|
189
|
+
}
|
|
190
|
+
#isEnabled({ link }) {
|
|
191
|
+
return !this.#host.isDisabled() && !link.isDisabled();
|
|
192
|
+
}
|
|
193
|
+
#keyTo(entry, event) {
|
|
194
|
+
const mapping = this.#host.mapping();
|
|
195
|
+
const from = entry.link.span();
|
|
196
|
+
const start = mapping.keyTarget(event.key, from.start);
|
|
197
|
+
if (start === void 0) return;
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
const next = moveRegionTo(mapping, from, start);
|
|
200
|
+
if (!event.repeat) {
|
|
201
|
+
this.#endKeyScrub();
|
|
202
|
+
if (entry.link.input(next)) entry.link.change();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (this.#keyScrub?.drawn !== entry || this.#keyScrub.key !== event.key) {
|
|
206
|
+
this.#endKeyScrub();
|
|
207
|
+
this.#keyScrub = {
|
|
208
|
+
drawn: entry,
|
|
209
|
+
from,
|
|
210
|
+
key: event.key
|
|
211
|
+
};
|
|
212
|
+
entry.link.hold("keys");
|
|
213
|
+
}
|
|
214
|
+
entry.link.input(next);
|
|
215
|
+
}
|
|
216
|
+
#redraw(region) {
|
|
217
|
+
if (this.#drawn.every((entry) => entry.region !== region)) {
|
|
218
|
+
this.render();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.#draw(region);
|
|
222
|
+
this.#dropGone();
|
|
223
|
+
}
|
|
224
|
+
#release(isRestored = false) {
|
|
225
|
+
const hold = this.#held;
|
|
226
|
+
if (!hold) return;
|
|
227
|
+
const { link } = hold.drawn;
|
|
228
|
+
this.#held = void 0;
|
|
229
|
+
if (isRestored) link.input(hold.from);
|
|
230
|
+
link.hold(void 0);
|
|
231
|
+
if (hasMoved(hold.from, link.span())) link.change();
|
|
232
|
+
}
|
|
233
|
+
#stripKey() {
|
|
234
|
+
const host = this.#host;
|
|
235
|
+
const mapping = host.mapping();
|
|
236
|
+
const [low, high] = mapping.bounds;
|
|
237
|
+
return [
|
|
238
|
+
low,
|
|
239
|
+
high,
|
|
240
|
+
mapping.proportionOf((low + high) / 2),
|
|
241
|
+
host.isDisabled(),
|
|
242
|
+
host.speak({
|
|
243
|
+
end: high,
|
|
244
|
+
start: high
|
|
245
|
+
})
|
|
246
|
+
].join("|");
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
export { RegionLayer };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SonicFormElement } from "./form-element.js";
|
|
2
|
+
|
|
3
|
+
//#region src/elements/region.ts
|
|
4
|
+
const owners = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
let link;
|
|
6
|
+
function linkRegion(element) {
|
|
7
|
+
return link(element);
|
|
8
|
+
}
|
|
9
|
+
function ownRegions(host, changed, signal) {
|
|
10
|
+
owners.set(host, changed);
|
|
11
|
+
signal.addEventListener("abort", () => {
|
|
12
|
+
if (owners.get(host) === changed) owners.delete(host);
|
|
13
|
+
}, { once: true });
|
|
14
|
+
}
|
|
15
|
+
var SonicRegion = class extends SonicFormElement {
|
|
16
|
+
static observedAttributes = [
|
|
17
|
+
...SonicFormElement.observedAttributes,
|
|
18
|
+
"end",
|
|
19
|
+
"kind",
|
|
20
|
+
"name",
|
|
21
|
+
"start"
|
|
22
|
+
];
|
|
23
|
+
static {
|
|
24
|
+
link = (element) => ({
|
|
25
|
+
change: () => {
|
|
26
|
+
element.#report("change");
|
|
27
|
+
},
|
|
28
|
+
draw: (part) => {
|
|
29
|
+
element.#draw(part);
|
|
30
|
+
},
|
|
31
|
+
hold: (by) => {
|
|
32
|
+
element.#held = by;
|
|
33
|
+
element.toggleState("dragging", by === "pointer");
|
|
34
|
+
},
|
|
35
|
+
input: (span) => element.#input(span),
|
|
36
|
+
isDisabled: () => element.isDisabled(),
|
|
37
|
+
span: () => {
|
|
38
|
+
const { end, start } = element.#span;
|
|
39
|
+
return {
|
|
40
|
+
end: Math.max(start, end),
|
|
41
|
+
start: Math.min(start, end)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
get dragging() {
|
|
47
|
+
return this.hasState("dragging");
|
|
48
|
+
}
|
|
49
|
+
get end() {
|
|
50
|
+
return this.#span.end;
|
|
51
|
+
}
|
|
52
|
+
set end(next) {
|
|
53
|
+
if (!this.#held) this.reflect("end", next);
|
|
54
|
+
}
|
|
55
|
+
/** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */
|
|
56
|
+
get kind() {
|
|
57
|
+
return this.getAttribute("kind") ?? void 0;
|
|
58
|
+
}
|
|
59
|
+
set kind(kind) {
|
|
60
|
+
this.reflect("kind", kind);
|
|
61
|
+
}
|
|
62
|
+
get start() {
|
|
63
|
+
return this.#span.start;
|
|
64
|
+
}
|
|
65
|
+
set start(next) {
|
|
66
|
+
if (!this.#held) this.reflect("start", next);
|
|
67
|
+
}
|
|
68
|
+
#formState = "";
|
|
69
|
+
#held;
|
|
70
|
+
#part;
|
|
71
|
+
#span = {
|
|
72
|
+
end: 0,
|
|
73
|
+
start: 0
|
|
74
|
+
};
|
|
75
|
+
attributeChangedCallback(name) {
|
|
76
|
+
const isSpan = name === "end" || name === "start";
|
|
77
|
+
if (isSpan && this.#held) return;
|
|
78
|
+
if (isSpan) this.#readSpan(name);
|
|
79
|
+
this.render();
|
|
80
|
+
}
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
this.upgradeProperties("end", "kind", "start");
|
|
83
|
+
super.connectedCallback();
|
|
84
|
+
}
|
|
85
|
+
formResetCallback() {
|
|
86
|
+
if (this.#held) return;
|
|
87
|
+
this.#readSpan("start");
|
|
88
|
+
this.#readSpan("end");
|
|
89
|
+
this.render();
|
|
90
|
+
}
|
|
91
|
+
connect() {
|
|
92
|
+
this.render();
|
|
93
|
+
if (this.parentElement?.localName !== "sonic-wavestrip");
|
|
94
|
+
}
|
|
95
|
+
focusTarget() {
|
|
96
|
+
return this.#part;
|
|
97
|
+
}
|
|
98
|
+
render() {
|
|
99
|
+
if (!this.isBound() || !this.parentElement) return;
|
|
100
|
+
owners.get(this.parentElement)?.(this);
|
|
101
|
+
}
|
|
102
|
+
restoreState(state) {
|
|
103
|
+
const [start = NaN, end = NaN] = state.split(",").map(Number);
|
|
104
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
105
|
+
this.#span = {
|
|
106
|
+
end,
|
|
107
|
+
start
|
|
108
|
+
};
|
|
109
|
+
this.render();
|
|
110
|
+
}
|
|
111
|
+
#draw(part) {
|
|
112
|
+
this.#part = part;
|
|
113
|
+
this.forwardNaming(part, true);
|
|
114
|
+
this.#writeForm();
|
|
115
|
+
}
|
|
116
|
+
#input(span) {
|
|
117
|
+
const hasMoved = span.start !== this.#span.start || span.end !== this.#span.end;
|
|
118
|
+
if (hasMoved) {
|
|
119
|
+
this.#span = span;
|
|
120
|
+
this.render();
|
|
121
|
+
this.#report("input");
|
|
122
|
+
}
|
|
123
|
+
return hasMoved;
|
|
124
|
+
}
|
|
125
|
+
#readSpan(name) {
|
|
126
|
+
const { end, start } = this.#span;
|
|
127
|
+
if (name === "end") {
|
|
128
|
+
this.#span = {
|
|
129
|
+
end: this.numberAttribute("end", start),
|
|
130
|
+
start
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const next = this.numberAttribute("start", 0);
|
|
135
|
+
this.#span = {
|
|
136
|
+
end: this.hasAttribute("end") ? end : next,
|
|
137
|
+
start: next
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
#report(type) {
|
|
141
|
+
this.dispatchEvent(new Event(type, {
|
|
142
|
+
bubbles: true,
|
|
143
|
+
composed: type === "input"
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
#writeForm() {
|
|
147
|
+
const { end, start } = this.#span;
|
|
148
|
+
const state = `${String(start)},${String(end)}`;
|
|
149
|
+
const formState = `${this.name}=${state}`;
|
|
150
|
+
if (formState === this.#formState) return;
|
|
151
|
+
this.#formState = formState;
|
|
152
|
+
this.writeFormFields([["start", start], ["end", end]], state);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
export { SonicRegion, linkRegion, ownRegions };
|