@xsynaptic/sonic-ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/default/dat.js +5 -4
- package/dist/default/define/button.js +1 -1
- package/dist/default/define.js +2 -2
- package/dist/default/elements/button.js +3 -3
- package/dist/default/elements/envelope.js +32 -16
- package/dist/default/elements/meter.js +11 -7
- package/dist/default/elements/number.js +1 -1
- package/dist/default/elements/radio-group.js +43 -45
- package/dist/default/elements/readout.js +1 -1
- package/dist/default/elements/sonic-element.js +56 -29
- package/dist/default/elements/switch.js +32 -38
- package/dist/default/elements/value-element.js +111 -108
- package/dist/default/elements/wave-element.js +5 -2
- package/dist/default/elements/waveform.js +35 -12
- package/dist/default/elements/wavestrip.js +18 -15
- package/dist/default/elements/xy.js +32 -18
- package/dist/default/index.js +2 -2
- package/dist/default/lib/adsr-shape.js +5 -0
- package/dist/default/lib/ballistics.js +2 -2
- package/dist/default/lib/canvas-surface.js +13 -2
- package/dist/default/lib/distribute.js +1 -1
- package/dist/default/lib/drag-step.js +4 -4
- package/dist/default/lib/frame-timeline.js +2 -1
- package/dist/default/lib/marker-band.js +54 -10
- package/dist/default/lib/marker-rider.js +10 -4
- package/dist/default/lib/math.js +6 -1
- package/dist/default/lib/pointer-drag.js +8 -5
- package/dist/default/lib/resample-peaks.js +11 -1
- package/dist/default/lib/write-attribute.js +1 -0
- package/dist/dev/dat.d.ts +2 -2
- package/dist/dev/dat.js +5 -4
- package/dist/dev/dat.js.map +1 -1
- package/dist/dev/define/button.js +1 -1
- package/dist/dev/define.d.ts +4 -4
- package/dist/dev/define.js +2 -2
- package/dist/dev/elements/button.js +3 -3
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/dial.d.ts +1 -1
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/envelope.js +32 -16
- package/dist/dev/elements/envelope.js.map +1 -1
- package/dist/dev/elements/meter.d.ts +1 -1
- package/dist/dev/elements/meter.js +11 -7
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/number.js +1 -1
- package/dist/dev/elements/number.js.map +1 -1
- package/dist/dev/elements/radio-group.js +43 -45
- package/dist/dev/elements/radio-group.js.map +1 -1
- package/dist/dev/elements/readout.js +1 -1
- package/dist/dev/elements/readout.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +1 -1
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/sonic-element.d.ts +4 -2
- package/dist/dev/elements/sonic-element.js +79 -29
- package/dist/dev/elements/sonic-element.js.map +1 -1
- package/dist/dev/elements/switch.js +32 -38
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +3 -3
- package/dist/dev/elements/value-element.js +111 -108
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/wave-element.d.ts +1 -0
- package/dist/dev/elements/wave-element.js +5 -2
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +1 -1
- package/dist/dev/elements/waveform.js +35 -12
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.js +18 -15
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.d.ts +3 -3
- package/dist/dev/elements/xy.js +32 -18
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +2 -2
- package/dist/dev/index.js +2 -2
- package/dist/dev/lib/adsr-shape.js +5 -0
- package/dist/dev/lib/adsr-shape.js.map +1 -1
- package/dist/dev/lib/ballistics.js +2 -2
- package/dist/dev/lib/ballistics.js.map +1 -1
- package/dist/dev/lib/canvas-surface.d.ts +1 -0
- package/dist/dev/lib/canvas-surface.js +13 -2
- package/dist/dev/lib/canvas-surface.js.map +1 -1
- package/dist/dev/lib/distribute.js +1 -1
- package/dist/dev/lib/distribute.js.map +1 -1
- package/dist/dev/lib/drag-step.js +4 -4
- package/dist/dev/lib/drag-step.js.map +1 -1
- package/dist/dev/lib/frame-timeline.js +2 -1
- package/dist/dev/lib/frame-timeline.js.map +1 -1
- package/dist/dev/lib/marker-band.js +54 -10
- package/dist/dev/lib/marker-band.js.map +1 -1
- package/dist/dev/lib/marker-rider.js +10 -4
- package/dist/dev/lib/marker-rider.js.map +1 -1
- package/dist/dev/lib/math.js +6 -1
- package/dist/dev/lib/math.js.map +1 -1
- package/dist/dev/lib/pointer-drag.js +8 -5
- package/dist/dev/lib/pointer-drag.js.map +1 -1
- package/dist/dev/lib/resample-peaks.js +11 -1
- package/dist/dev/lib/resample-peaks.js.map +1 -1
- package/dist/dev/lib/write-attribute.js +1 -0
- package/dist/dev/lib/write-attribute.js.map +1 -1
- package/dist/skins/amber.css +8 -0
- package/dist/skins/flat.css +25 -0
- package/dist/skins/ivory.css +1 -0
- package/dist/skins/lime.css +2 -0
- package/dist/skins/slate.css +2 -0
- package/dist/styles/button.css +38 -10
- package/dist/styles/dial.css +24 -16
- package/dist/styles/envelope.css +56 -11
- package/dist/styles/led.css +3 -3
- package/dist/styles/material.css +83 -41
- package/dist/styles/meter.css +8 -4
- package/dist/styles/panel.css +5 -3
- package/dist/styles/segmented.css +27 -5
- package/dist/styles/slider.css +2 -1
- package/dist/styles/switch.css +59 -20
- package/dist/styles/waveform.css +28 -5
- package/dist/styles/wavestrip.css +23 -6
- package/dist/styles/xy.css +61 -24
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @xsynaptic/sonic-ui
|
|
2
2
|
|
|
3
|
-
Native custom elements for audio interfaces. Each control renders in the light DOM,
|
|
3
|
+
Native custom elements for audio interfaces. Each control renders in the light DOM, skinned with `--sonic-*` tokens (CSS custom properties). No framework, no runtime dependencies.
|
|
4
4
|
|
|
5
5
|
Early and unstable: any 0.x release may break the API.
|
|
6
6
|
|
|
@@ -23,4 +23,6 @@ import '@xsynaptic/sonic-ui/define';
|
|
|
23
23
|
<sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Under a hydrating framework, register the definitions once hydration is done (in React, from an effect); a control owns one child of its host and uses only the children it documents.
|
|
27
|
+
|
|
26
28
|
Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
|
package/dist/default/dat.js
CHANGED
|
@@ -16,6 +16,7 @@ function readDatHeader(buffer) {
|
|
|
16
16
|
if (channels < 1) throw new Error("A .dat header names no channels");
|
|
17
17
|
const sampleRate = view.getInt32(8, true);
|
|
18
18
|
const samplesPerPixel = view.getInt32(12, true);
|
|
19
|
+
if (samplesPerPixel < 1) throw new Error("A .dat header names no samples per pixel");
|
|
19
20
|
return {
|
|
20
21
|
bits: (view.getUint32(4, true) & 1) === 1 ? 8 : 16,
|
|
21
22
|
channels,
|
|
@@ -34,15 +35,15 @@ function datByteRange(header, fromPair, toPair) {
|
|
|
34
35
|
const pairBytes = header.channels * 2 * header.bits / 8;
|
|
35
36
|
return [header.dataOffset + from * pairBytes, header.dataOffset + to * pairBytes - 1];
|
|
36
37
|
}
|
|
37
|
-
function readDatSamples(header, body) {
|
|
38
|
-
return header.bits === 8 ? new Int8Array(body) : new Int16Array(body,
|
|
38
|
+
function readDatSamples(header, body, byteOffset = 0) {
|
|
39
|
+
return header.bits === 8 ? new Int8Array(body, byteOffset) : new Int16Array(body, byteOffset, Math.floor((body.byteLength - byteOffset) / 2));
|
|
39
40
|
}
|
|
40
|
-
function readDatPeaks(header, body) {
|
|
41
|
+
function readDatPeaks(header, body, byteOffset = 0) {
|
|
41
42
|
return {
|
|
42
43
|
channels: header.channels,
|
|
43
44
|
fullScale: header.bits === 8 ? 128 : 32768,
|
|
44
45
|
pairsPerSecond: header.pairsPerSecond,
|
|
45
|
-
samples: readDatSamples(header, body)
|
|
46
|
+
samples: readDatSamples(header, body, byteOffset)
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
package/dist/default/define.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import "./define/button.js";
|
|
1
2
|
import "./define/dial.js";
|
|
2
3
|
import "./define/envelope.js";
|
|
3
|
-
import "./define/button.js";
|
|
4
|
-
import "./define/switch.js";
|
|
5
4
|
import "./define/meter.js";
|
|
6
5
|
import "./define/number.js";
|
|
7
6
|
import "./define/segmented.js";
|
|
8
7
|
import "./define/slider.js";
|
|
9
8
|
import "./define/split.js";
|
|
9
|
+
import "./define/switch.js";
|
|
10
10
|
import "./define/waveform.js";
|
|
11
11
|
import "./define/wavestrip.js";
|
|
12
12
|
import "./define/xy.js";
|
|
@@ -113,12 +113,12 @@ var SonicButton = class extends SonicFormElement {
|
|
|
113
113
|
return this.#button;
|
|
114
114
|
}
|
|
115
115
|
render() {
|
|
116
|
+
if (!this.isBound()) return;
|
|
116
117
|
const button = this.#button;
|
|
117
|
-
button
|
|
118
|
+
writeAttribute(button, "disabled", this.isDisabled() ? "" : void 0);
|
|
118
119
|
writeAttribute(button, "aria-disabled", this.#isSoftDisabled() ? "true" : void 0);
|
|
119
120
|
writeAttribute(button, "aria-busy", this.busy ? "true" : void 0);
|
|
120
|
-
button
|
|
121
|
-
if (this.latching) button.setAttribute("aria-pressed", String(this.pressed));
|
|
121
|
+
writeAttribute(button, "aria-pressed", this.latching ? String(this.pressed) : void 0);
|
|
122
122
|
this.toggleState("pressed", this.pressed);
|
|
123
123
|
this.forwardNaming(button, true);
|
|
124
124
|
const submitted = this.latching && this.pressed ? this.value : null;
|
|
@@ -4,10 +4,10 @@ import { SonicElement } from "./sonic-element.js";
|
|
|
4
4
|
import { ReadoutClaim } from "./readout-claim.js";
|
|
5
5
|
import { Readout } from "./readout.js";
|
|
6
6
|
import { dragThresholdPx } from "../lib/drag-step.js";
|
|
7
|
+
import { bindDrag } from "../lib/pointer-drag.js";
|
|
7
8
|
import { SonicValueElement, linkValue } from "./value-element.js";
|
|
8
9
|
import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
|
|
9
10
|
import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
|
|
10
|
-
import { bindDrag } from "../lib/pointer-drag.js";
|
|
11
11
|
|
|
12
12
|
//#region src/elements/envelope.ts
|
|
13
13
|
const curveStages = [
|
|
@@ -31,10 +31,22 @@ function flip(point, isFlipped) {
|
|
|
31
31
|
function isParent(node) {
|
|
32
32
|
return "querySelector" in node;
|
|
33
33
|
}
|
|
34
|
+
const emptyPath = {
|
|
35
|
+
fill: "",
|
|
36
|
+
floor: {
|
|
37
|
+
from: 0,
|
|
38
|
+
to: 0
|
|
39
|
+
},
|
|
40
|
+
stroke: ""
|
|
41
|
+
};
|
|
42
|
+
function stageParts(envelope, hook, stages) {
|
|
43
|
+
return new Map(stages.map((stage) => [stage, requireChild(envelope, `.${hook}[data-sonic-stage="${stage}"]`, HTMLDivElement)]));
|
|
44
|
+
}
|
|
34
45
|
const renderEnvelope = template(`
|
|
35
46
|
<div class="sonic-envelope" aria-hidden="true">
|
|
36
47
|
<svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
|
|
37
48
|
<path class="sonic-envelope-fill" />
|
|
49
|
+
<rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
|
|
38
50
|
<path class="sonic-envelope-line" />
|
|
39
51
|
</svg>
|
|
40
52
|
<div class="sonic-envelope-handle" data-sonic-stage="delay" hidden></div>
|
|
@@ -119,8 +131,12 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
119
131
|
this.#renderReadout();
|
|
120
132
|
});
|
|
121
133
|
#envelope = renderEnvelope();
|
|
134
|
+
#curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
|
|
122
135
|
#fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
|
|
136
|
+
#floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
|
|
123
137
|
#graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
|
|
138
|
+
#handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
|
|
139
|
+
#isDriving = false;
|
|
124
140
|
#line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
|
|
125
141
|
#pointerDrag;
|
|
126
142
|
#readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
|
|
@@ -145,7 +161,7 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
145
161
|
connect(signal) {
|
|
146
162
|
const envelope = this.#envelope;
|
|
147
163
|
this.#signal = signal;
|
|
148
|
-
this.
|
|
164
|
+
this.keepControl(envelope, signal);
|
|
149
165
|
this.#bind();
|
|
150
166
|
this.checkStyles(envelope, "envelope.css");
|
|
151
167
|
this.#bindPointer(envelope, signal);
|
|
@@ -162,7 +178,7 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
162
178
|
if (!element) continue;
|
|
163
179
|
const link = linkValue(element);
|
|
164
180
|
const unwatch = link.watch(() => {
|
|
165
|
-
this.#draw();
|
|
181
|
+
if (!this.#isDriving) this.#draw();
|
|
166
182
|
});
|
|
167
183
|
this.#bindings.set(name, {
|
|
168
184
|
element,
|
|
@@ -202,32 +218,30 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
202
218
|
}
|
|
203
219
|
#draw() {
|
|
204
220
|
const shape = this.#readShape();
|
|
205
|
-
const path = this.#bindings.size === 0 ?
|
|
206
|
-
fill: "",
|
|
207
|
-
stroke: ""
|
|
208
|
-
} : adsrPath(shape);
|
|
221
|
+
const path = this.#bindings.size === 0 ? emptyPath : adsrPath(shape);
|
|
209
222
|
this.#shape = shape;
|
|
210
|
-
this.#line
|
|
211
|
-
this.#fill
|
|
212
|
-
|
|
223
|
+
writeAttribute(this.#line, "d", path.stroke);
|
|
224
|
+
writeAttribute(this.#fill, "d", path.fill);
|
|
225
|
+
writeAttribute(this.#floor, "x", String(path.floor.from));
|
|
226
|
+
writeAttribute(this.#floor, "width", String(path.floor.to - path.floor.from));
|
|
227
|
+
for (const [stage, part] of this.#handles) this.#drawPoint(part, {
|
|
213
228
|
at: shape.handles.find((point) => point.stage === stage),
|
|
214
229
|
drives: this.#drives(stage)
|
|
215
230
|
});
|
|
216
|
-
for (const stage of
|
|
231
|
+
for (const [stage, part] of this.#curveHandles) this.#drawPoint(part, {
|
|
217
232
|
at: shape.curveHandles.find((point) => point.stage === stage),
|
|
218
233
|
drives: this.#curveDrives(stage)
|
|
219
234
|
});
|
|
220
235
|
this.#renderReadout();
|
|
221
236
|
}
|
|
222
|
-
#drawPoint(
|
|
223
|
-
const part = requireChild(this.#envelope, selector, HTMLDivElement);
|
|
237
|
+
#drawPoint(part, point) {
|
|
224
238
|
const { at, drives } = point;
|
|
225
|
-
part
|
|
239
|
+
writeAttribute(part, "hidden", at ? void 0 : "");
|
|
226
240
|
if (!at) return;
|
|
227
241
|
const isStill = this.isDisabled() || drives.every(({ binding }) => binding.link.isDisabled());
|
|
228
242
|
part.style.setProperty("--_sonic-envelope-x", String(at.x));
|
|
229
243
|
part.style.setProperty("--_sonic-envelope-y", String(at.y));
|
|
230
|
-
part
|
|
244
|
+
writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
|
|
231
245
|
writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
|
|
232
246
|
}
|
|
233
247
|
#drives(stage) {
|
|
@@ -284,11 +298,13 @@ var SonicEnvelope = class extends SonicElement {
|
|
|
284
298
|
const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
|
|
285
299
|
drag.state = step.state;
|
|
286
300
|
if (step.state.isEngaged) this.#claim.reveal("drag");
|
|
301
|
+
this.#isDriving = true;
|
|
287
302
|
for (const { axis, binding } of drag.drives) {
|
|
288
303
|
const next = step[axis];
|
|
289
304
|
if (next !== void 0) binding.link.input(next);
|
|
290
305
|
}
|
|
291
|
-
this.#
|
|
306
|
+
this.#isDriving = false;
|
|
307
|
+
this.#draw();
|
|
292
308
|
}
|
|
293
309
|
#pressed(event) {
|
|
294
310
|
const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toNumber } from "../lib/math.js";
|
|
1
2
|
import { template } from "../lib/render.js";
|
|
2
3
|
import { SonicElement } from "./sonic-element.js";
|
|
3
4
|
import { parseNumberList } from "../lib/number-list.js";
|
|
@@ -51,7 +52,8 @@ var SonicMeter = class extends SonicElement {
|
|
|
51
52
|
get level() {
|
|
52
53
|
return this.#level;
|
|
53
54
|
}
|
|
54
|
-
set level(
|
|
55
|
+
set level(level) {
|
|
56
|
+
const amplitude = toNumber(level);
|
|
55
57
|
if (!Number.isFinite(amplitude)) return;
|
|
56
58
|
const decibels = toDecibels(amplitude);
|
|
57
59
|
this.#level = amplitude;
|
|
@@ -121,7 +123,8 @@ var SonicMeter = class extends SonicElement {
|
|
|
121
123
|
const now = performance.now();
|
|
122
124
|
if (name === "ballistics") this.#velocity = 0;
|
|
123
125
|
else if (name === "value") this.#readValue(now);
|
|
124
|
-
this.#
|
|
126
|
+
this.#segments = this.segments;
|
|
127
|
+
if (name === "segments") this.#renderSegments();
|
|
125
128
|
this.#render(now);
|
|
126
129
|
this.#schedule(now);
|
|
127
130
|
}
|
|
@@ -131,7 +134,7 @@ var SonicMeter = class extends SonicElement {
|
|
|
131
134
|
connect(signal) {
|
|
132
135
|
const now = performance.now();
|
|
133
136
|
this.upgradeProperties("ballistics", "max", "min", "orientation", "origin", "segments", "value", "level");
|
|
134
|
-
this.
|
|
137
|
+
this.keepControl(this.#meter, signal);
|
|
135
138
|
this.#renderSegments();
|
|
136
139
|
this.#render(now);
|
|
137
140
|
this.#schedule(now);
|
|
@@ -165,6 +168,7 @@ var SonicMeter = class extends SonicElement {
|
|
|
165
168
|
if (value !== void 0) this.#peak = Math.max(this.#peak, value);
|
|
166
169
|
}
|
|
167
170
|
#render(now) {
|
|
171
|
+
if (!this.isBound()) return;
|
|
168
172
|
const meter = this.#meter;
|
|
169
173
|
const origin = this.origin === void 0 ? 0 : this.#proportionOf(this.origin);
|
|
170
174
|
meter.style.setProperty("--_sonic-meter-min", String(this.min));
|
|
@@ -185,9 +189,9 @@ var SonicMeter = class extends SonicElement {
|
|
|
185
189
|
meter.style.setProperty("--_sonic-meter-clipped", now - this.#clipAt < holdMs ? "1" : "0");
|
|
186
190
|
}
|
|
187
191
|
#renderSegments() {
|
|
192
|
+
if (!this.isBound()) return;
|
|
188
193
|
const meter = this.#meter;
|
|
189
|
-
const segments = this
|
|
190
|
-
this.#segments = segments;
|
|
194
|
+
const segments = this.#segments;
|
|
191
195
|
this.toggleState("ladder", segments !== void 0);
|
|
192
196
|
if (!segments) {
|
|
193
197
|
for (const name of [
|
|
@@ -226,9 +230,9 @@ var SonicMeter = class extends SonicElement {
|
|
|
226
230
|
this.#stepNeedle(elapsedMs);
|
|
227
231
|
return;
|
|
228
232
|
}
|
|
229
|
-
const { fallDecibelsPerSecond,
|
|
233
|
+
const { fallDecibelsPerSecond, riseTimeConstantMs } = ballisticsRates[mode];
|
|
230
234
|
const floor = Math.max(this.#target, this.min);
|
|
231
|
-
this.#bar = this.#target > this.#bar ? this.#risen(
|
|
235
|
+
this.#bar = this.#target > this.#bar ? this.#risen(riseTimeConstantMs, elapsedMs) : fall(this.#bar, {
|
|
232
236
|
fallDecibelsPerSecond,
|
|
233
237
|
floor
|
|
234
238
|
}, elapsedMs);
|
|
@@ -30,7 +30,7 @@ var SonicNumber = class extends SonicValueElement {
|
|
|
30
30
|
}
|
|
31
31
|
connect(signal) {
|
|
32
32
|
const number = this.#number;
|
|
33
|
-
this.
|
|
33
|
+
this.keepControl(number, signal);
|
|
34
34
|
this.render();
|
|
35
35
|
this.checkStyles(number, "number.css");
|
|
36
36
|
this.bindGestures(number, signal, () => ({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { placeChildren } from "../lib/render.js";
|
|
2
|
+
import { writeAttribute } from "../lib/write-attribute.js";
|
|
2
3
|
import { SonicFormElement } from "./form-element.js";
|
|
3
4
|
import { copyNode } from "../lib/copy-node.js";
|
|
5
|
+
import { bindDrag } from "../lib/pointer-drag.js";
|
|
4
6
|
|
|
5
7
|
//#region src/elements/radio-group.ts
|
|
6
8
|
const keySteps = /* @__PURE__ */ new Map([
|
|
@@ -9,10 +11,12 @@ const keySteps = /* @__PURE__ */ new Map([
|
|
|
9
11
|
["ArrowRight", 1],
|
|
10
12
|
["ArrowUp", -1]
|
|
11
13
|
]);
|
|
12
|
-
function
|
|
13
|
-
const box = option.getBoundingClientRect();
|
|
14
|
+
function isInside(box, x, y) {
|
|
14
15
|
return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
|
|
15
16
|
}
|
|
17
|
+
function isUnder(option, x, y) {
|
|
18
|
+
return isInside(option.getBoundingClientRect(), x, y);
|
|
19
|
+
}
|
|
16
20
|
function optionValue(option) {
|
|
17
21
|
return option.querySelector("[data-sonic-value]")?.dataset.sonicValue;
|
|
18
22
|
}
|
|
@@ -26,7 +30,6 @@ var SonicRadioGroupElement = class extends SonicFormElement {
|
|
|
26
30
|
this.#value = next;
|
|
27
31
|
this.render();
|
|
28
32
|
}
|
|
29
|
-
#press;
|
|
30
33
|
#value = "";
|
|
31
34
|
attributeChangedCallback(name) {
|
|
32
35
|
if (name === "value") this.#value = this.getAttribute("value") ?? "";
|
|
@@ -86,13 +89,15 @@ var SonicRadioGroupElement = class extends SonicFormElement {
|
|
|
86
89
|
return option;
|
|
87
90
|
}
|
|
88
91
|
render() {
|
|
92
|
+
if (!this.isBound()) return;
|
|
89
93
|
const options = this.options();
|
|
90
94
|
const checked = options.find((option) => optionValue(option) === this.#value);
|
|
91
95
|
const focusable = checked ?? options[0];
|
|
96
|
+
const isDisabled = this.isDisabled();
|
|
92
97
|
for (const option of options) {
|
|
93
|
-
option
|
|
94
|
-
option
|
|
95
|
-
option
|
|
98
|
+
writeAttribute(option, "disabled", isDisabled ? "" : void 0);
|
|
99
|
+
writeAttribute(option, "aria-checked", String(option === checked));
|
|
100
|
+
writeAttribute(option, "tabindex", option === focusable ? "0" : "-1");
|
|
96
101
|
}
|
|
97
102
|
this.forwardNaming(this.group, true);
|
|
98
103
|
this.writeFormValue(checked ? this.#value : null, this.#value);
|
|
@@ -107,41 +112,32 @@ var SonicRadioGroupElement = class extends SonicFormElement {
|
|
|
107
112
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
108
113
|
}
|
|
109
114
|
#bindPress(group, signal) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!option) return;
|
|
137
|
-
this.select(option);
|
|
138
|
-
if (group.matches(":focus-within")) option.focus();
|
|
139
|
-
}, { signal });
|
|
140
|
-
group.addEventListener("pointercancel", release, { signal });
|
|
141
|
-
group.addEventListener("lostpointercapture", release, { signal });
|
|
142
|
-
signal.addEventListener("abort", () => {
|
|
143
|
-
this.#hold(void 0);
|
|
144
|
-
}, { once: true });
|
|
115
|
+
bindDrag(group, {
|
|
116
|
+
grab: (event) => {
|
|
117
|
+
const option = this.optionOf(event.target);
|
|
118
|
+
if (!option || this.isDisabled() || this.claimPress(option, event)) return;
|
|
119
|
+
const boxes = this.#optionBoxes();
|
|
120
|
+
this.#markPressed(option);
|
|
121
|
+
return {
|
|
122
|
+
boxes,
|
|
123
|
+
option
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
lift: (press) => {
|
|
127
|
+
if (!press.option || this.isDisabled()) return;
|
|
128
|
+
const option = this.releaseTarget(press.option);
|
|
129
|
+
if (!option) return;
|
|
130
|
+
this.select(option);
|
|
131
|
+
if (group.matches(":focus-within")) option.focus();
|
|
132
|
+
},
|
|
133
|
+
move: (press, event) => {
|
|
134
|
+
press.option = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
|
|
135
|
+
this.#markPressed(press.option);
|
|
136
|
+
},
|
|
137
|
+
release: () => {
|
|
138
|
+
this.#markPressed(void 0);
|
|
139
|
+
}
|
|
140
|
+
}, signal);
|
|
145
141
|
}
|
|
146
142
|
#copyOptions(copies) {
|
|
147
143
|
const group = this.group;
|
|
@@ -160,10 +156,6 @@ var SonicRadioGroupElement = class extends SonicFormElement {
|
|
|
160
156
|
this.render();
|
|
161
157
|
if (focused) this.#refocus(focused, focusedValue);
|
|
162
158
|
}
|
|
163
|
-
#hold(press) {
|
|
164
|
-
this.#press = press;
|
|
165
|
-
for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === press?.option);
|
|
166
|
-
}
|
|
167
159
|
#keyTarget(option, key) {
|
|
168
160
|
const options = this.options();
|
|
169
161
|
if (key === "Home") return options[0];
|
|
@@ -174,6 +166,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
|
|
|
174
166
|
if (this.isWrapping()) return options[(index + options.length) % options.length];
|
|
175
167
|
return options[Math.min(Math.max(index, 0), options.length - 1)];
|
|
176
168
|
}
|
|
169
|
+
#markPressed(pressed) {
|
|
170
|
+
for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
|
|
171
|
+
}
|
|
172
|
+
#optionBoxes() {
|
|
173
|
+
return this.options().map((option) => [option, option.getBoundingClientRect()]);
|
|
174
|
+
}
|
|
177
175
|
#refocus(focused, value) {
|
|
178
176
|
const options = this.options();
|
|
179
177
|
const next = options.find((option) => optionValue(option) === value) ?? options.find((option) => option.tabIndex === 0);
|
|
@@ -23,7 +23,7 @@ var Readout = class {
|
|
|
23
23
|
this.#anchor = anchor;
|
|
24
24
|
}
|
|
25
25
|
this.#text.hidden = text === void 0;
|
|
26
|
-
if (text !== void 0) this.#text.textContent = text;
|
|
26
|
+
if (isOpen && text !== void 0 && this.#text.textContent !== text) this.#text.textContent = text;
|
|
27
27
|
this.#toggle(isOpen);
|
|
28
28
|
}
|
|
29
29
|
#toggle(isOpen) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toNumber } from "../lib/math.js";
|
|
1
2
|
import { attachSlots } from "../lib/render.js";
|
|
2
3
|
import { writeAttribute } from "../lib/write-attribute.js";
|
|
3
4
|
|
|
@@ -30,13 +31,14 @@ var SonicElement = class extends HTMLElement {
|
|
|
30
31
|
this.upgradeProperties("disabled");
|
|
31
32
|
if (this.#connection) return;
|
|
32
33
|
const connection = new AbortController();
|
|
34
|
+
this.#connection = connection;
|
|
33
35
|
try {
|
|
34
36
|
this.connect(connection.signal);
|
|
35
37
|
} catch (error) {
|
|
36
38
|
connection.abort();
|
|
39
|
+
this.#connection = void 0;
|
|
37
40
|
throw error;
|
|
38
41
|
}
|
|
39
|
-
this.#connection = connection;
|
|
40
42
|
}
|
|
41
43
|
connectedMoveCallback() {}
|
|
42
44
|
disconnectedCallback() {
|
|
@@ -56,12 +58,6 @@ var SonicElement = class extends HTMLElement {
|
|
|
56
58
|
}
|
|
57
59
|
if (!this.isDisabled() && !this.#focused()) target.focus(options);
|
|
58
60
|
}
|
|
59
|
-
appendOnce(control) {
|
|
60
|
-
const [hook] = control.classList;
|
|
61
|
-
const stale = [...this.children].filter((child) => child !== control && hook !== void 0 && child.classList.contains(hook));
|
|
62
|
-
for (const child of stale) child.remove();
|
|
63
|
-
if (control.parentNode !== this) this.append(control);
|
|
64
|
-
}
|
|
65
61
|
checkStyles(control, sheet) {}
|
|
66
62
|
focusTarget() {}
|
|
67
63
|
forwardNaming(target, isNamed) {
|
|
@@ -73,36 +69,64 @@ var SonicElement = class extends HTMLElement {
|
|
|
73
69
|
}
|
|
74
70
|
if (labels.length > 0) writeLabelledBy(target, labels);
|
|
75
71
|
}
|
|
72
|
+
isBound() {
|
|
73
|
+
return this.#connection !== void 0;
|
|
74
|
+
}
|
|
76
75
|
isDisabled() {
|
|
77
76
|
return this.disabled || this.matches(":disabled");
|
|
78
77
|
}
|
|
79
|
-
|
|
78
|
+
keepControl(control, signal) {
|
|
79
|
+
const keep = () => {
|
|
80
|
+
this.#appendOnce(control);
|
|
81
|
+
this.#checkChildren([...this.childNodes].filter((child) => child !== control));
|
|
82
|
+
};
|
|
83
|
+
const observer = new MutationObserver(keep);
|
|
84
|
+
keep();
|
|
85
|
+
observer.observe(this, { childList: true });
|
|
86
|
+
signal.addEventListener("abort", () => {
|
|
87
|
+
observer.disconnect();
|
|
88
|
+
}, { once: true });
|
|
89
|
+
}
|
|
90
|
+
mirrorChildren({ control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
|
|
80
91
|
const slots = this.#slots ?? attachSlots(this);
|
|
81
92
|
let copies = /* @__PURE__ */ new Map();
|
|
82
93
|
this.#slots = slots;
|
|
83
94
|
const mirror = (touched) => {
|
|
84
|
-
this
|
|
95
|
+
this.#appendOnce(control);
|
|
85
96
|
const children = [...this.childNodes].filter((child) => child !== control);
|
|
86
97
|
const slotted = children.filter((child) => child instanceof Element || child instanceof Text);
|
|
87
98
|
const kept = copies;
|
|
88
|
-
|
|
99
|
+
const originals = children.filter((child) => isCopied(child));
|
|
100
|
+
this.#checkMirrored(originals);
|
|
101
|
+
this.#checkChildren(children.filter((child) => !isCopied(child) && !isPassed(child)));
|
|
102
|
+
copies = new Map(originals.map((original) => [original, touched && !touched.has(original) && kept.has(original) ? kept.get(original) : copy(original)]));
|
|
89
103
|
place([...copies.values()].filter((made) => made !== void 0));
|
|
90
104
|
slots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);
|
|
91
105
|
slots.kept.assign(...slotted.filter((child) => isCopied(child)));
|
|
92
106
|
};
|
|
107
|
+
const watch = () => {
|
|
108
|
+
observer.disconnect();
|
|
109
|
+
observer.observe(this, { childList: true });
|
|
110
|
+
for (const child of this.childNodes) {
|
|
111
|
+
if (child === control) continue;
|
|
112
|
+
observer.observe(child, {
|
|
113
|
+
attributes: true,
|
|
114
|
+
characterData: true,
|
|
115
|
+
childList: true,
|
|
116
|
+
subtree: true
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
93
120
|
const observer = new MutationObserver((records) => {
|
|
94
121
|
const changes = records.filter((record) => this.#isCopiedChange(record, control, isCopied));
|
|
95
122
|
if (changes.length === 0) return;
|
|
96
|
-
if (changes.some((record) => record.target === this))
|
|
97
|
-
|
|
123
|
+
if (changes.some((record) => record.target === this)) {
|
|
124
|
+
mirror();
|
|
125
|
+
watch();
|
|
126
|
+
} else mirror(new Set(changes.map((record) => this.#childHolding(record.target))));
|
|
98
127
|
});
|
|
99
128
|
mirror();
|
|
100
|
-
|
|
101
|
-
attributes: true,
|
|
102
|
-
characterData: true,
|
|
103
|
-
childList: true,
|
|
104
|
-
subtree: true
|
|
105
|
-
});
|
|
129
|
+
watch();
|
|
106
130
|
signal.addEventListener("abort", () => {
|
|
107
131
|
observer.disconnect();
|
|
108
132
|
}, { once: true });
|
|
@@ -111,9 +135,7 @@ var SonicElement = class extends HTMLElement {
|
|
|
111
135
|
return [];
|
|
112
136
|
}
|
|
113
137
|
numberAttribute(name, fallback) {
|
|
114
|
-
const
|
|
115
|
-
if (attribute === null || attribute.trim() === "") return fallback;
|
|
116
|
-
const parsed = Number(attribute);
|
|
138
|
+
const parsed = toNumber(this.getAttribute(name));
|
|
117
139
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
118
140
|
}
|
|
119
141
|
optionalNumberAttribute(name) {
|
|
@@ -121,11 +143,11 @@ var SonicElement = class extends HTMLElement {
|
|
|
121
143
|
return Number.isNaN(value) ? void 0 : value;
|
|
122
144
|
}
|
|
123
145
|
reflect(name, value) {
|
|
124
|
-
if (value
|
|
125
|
-
this.
|
|
146
|
+
if (typeof value !== "number" && typeof value !== "string" && value !== true) {
|
|
147
|
+
this.removeAttribute(name);
|
|
126
148
|
return;
|
|
127
149
|
}
|
|
128
|
-
|
|
150
|
+
this.setAttribute(name, value === true ? "" : value.toString());
|
|
129
151
|
}
|
|
130
152
|
toggleState(state, isOn) {
|
|
131
153
|
if (isOn) this.internals?.states.add(state);
|
|
@@ -139,6 +161,14 @@ var SonicElement = class extends HTMLElement {
|
|
|
139
161
|
this[name] = value;
|
|
140
162
|
}
|
|
141
163
|
}
|
|
164
|
+
#appendOnce(control) {
|
|
165
|
+
const [hook] = control.classList;
|
|
166
|
+
const stale = [...this.children].filter((child) => child !== control && hook !== void 0 && child.classList.contains(hook));
|
|
167
|
+
for (const child of stale) child.remove();
|
|
168
|
+
if (control.parentNode !== this) this.append(control);
|
|
169
|
+
}
|
|
170
|
+
#checkChildren(undocumented) {}
|
|
171
|
+
#checkMirrored(originals) {}
|
|
142
172
|
#childHolding(node) {
|
|
143
173
|
let current = node;
|
|
144
174
|
while (current && current.parentNode !== this) current = current.parentNode;
|
|
@@ -150,12 +180,9 @@ var SonicElement = class extends HTMLElement {
|
|
|
150
180
|
return active instanceof HTMLElement && this.contains(active) ? active : void 0;
|
|
151
181
|
}
|
|
152
182
|
#isCopiedChange(record, control, isCopied) {
|
|
153
|
-
if (record.target === this)
|
|
154
|
-
if (record.type === "attributes") return false;
|
|
155
|
-
return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
|
|
156
|
-
}
|
|
183
|
+
if (record.target === this) return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
|
|
157
184
|
const child = this.#childHolding(record.target);
|
|
158
|
-
if (child === void 0
|
|
185
|
+
if (child === void 0) return false;
|
|
159
186
|
return isCopied(child) || record.type === "attributes" && record.target === child;
|
|
160
187
|
}
|
|
161
188
|
};
|