@xsynaptic/sonic-ui 0.1.0 → 0.3.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 +8 -1
- package/dist/default/define/dial.js +7 -0
- package/dist/default/define/key.js +7 -0
- package/dist/default/define/meter.js +7 -0
- package/dist/default/define/number.js +7 -0
- package/dist/default/define/segmented.js +7 -0
- package/dist/default/define/slider.js +7 -0
- package/dist/{define.js → default/define.js} +1 -0
- package/dist/default/elements/define-once.js +7 -0
- package/dist/default/elements/dial.js +81 -0
- package/dist/default/elements/form-element.js +56 -0
- package/dist/default/elements/key.js +144 -0
- package/dist/{elements → default/elements}/meter.js +27 -16
- package/dist/default/elements/number.js +46 -0
- package/dist/default/elements/range-element.js +433 -0
- package/dist/{elements → default/elements}/range-entry.js +5 -4
- package/dist/default/elements/segmented.js +173 -0
- package/dist/default/elements/slider.js +99 -0
- package/dist/default/elements/sonic-element.js +153 -0
- package/dist/{index.js → default/index.js} +3 -1
- package/dist/default/lib/copy-node.js +30 -0
- package/dist/default/lib/percent.js +12 -0
- package/dist/default/lib/read-px-property.js +8 -0
- package/dist/{lib → default/lib}/render.js +9 -2
- package/dist/{lib → default/lib}/taper.js +4 -2
- package/dist/default/lib/write-attribute.js +11 -0
- package/dist/dev/define/dial.js.map +1 -0
- package/dist/dev/define/key.js.map +1 -0
- package/dist/dev/define/meter.js.map +1 -0
- package/dist/dev/define/number.d.ts +2 -0
- package/dist/dev/define/number.js +8 -0
- package/dist/dev/define/number.js.map +1 -0
- package/dist/dev/define/segmented.js.map +1 -0
- package/dist/dev/define/slider.js.map +1 -0
- package/dist/{define.d.ts → dev/define.d.ts} +3 -1
- package/dist/dev/define.js +6 -0
- package/dist/dev/elements/define-once.js.map +1 -0
- package/dist/{elements → dev/elements}/dial.d.ts +7 -0
- package/dist/{elements → dev/elements}/dial.js +31 -6
- package/dist/dev/elements/dial.js.map +1 -0
- package/dist/dev/elements/form-element.d.ts +22 -0
- package/dist/dev/elements/form-element.js +57 -0
- package/dist/dev/elements/form-element.js.map +1 -0
- package/dist/dev/elements/key.d.ts +30 -0
- package/dist/{elements → dev/elements}/key.js +71 -25
- package/dist/dev/elements/key.js.map +1 -0
- package/dist/{elements → dev/elements}/meter.d.ts +6 -0
- package/dist/dev/elements/meter.js +131 -0
- package/dist/dev/elements/meter.js.map +1 -0
- package/dist/dev/elements/number.d.ts +16 -0
- package/dist/dev/elements/number.js +47 -0
- package/dist/dev/elements/number.js.map +1 -0
- package/dist/dev/elements/range-element.d.ts +56 -0
- package/dist/{elements → dev/elements}/range-element.js +153 -67
- package/dist/dev/elements/range-element.js.map +1 -0
- package/dist/dev/elements/range-entry.js +75 -0
- package/dist/dev/elements/range-entry.js.map +1 -0
- package/dist/{elements → dev/elements}/segmented.d.ts +5 -2
- package/dist/dev/elements/segmented.js +174 -0
- package/dist/dev/elements/segmented.js.map +1 -0
- package/dist/{elements → dev/elements}/slider.d.ts +8 -0
- package/dist/{elements → dev/elements}/slider.js +37 -7
- package/dist/dev/elements/slider.js.map +1 -0
- package/dist/dev/elements/sonic-element.d.ts +33 -0
- package/dist/dev/elements/sonic-element.js +169 -0
- package/dist/dev/elements/sonic-element.js.map +1 -0
- package/dist/{index.d.ts → dev/index.d.ts} +3 -1
- package/dist/dev/index.js +9 -0
- package/dist/dev/lib/copy-node.js +31 -0
- package/dist/dev/lib/copy-node.js.map +1 -0
- package/dist/dev/lib/percent.d.ts +5 -0
- package/dist/dev/lib/percent.js +13 -0
- package/dist/dev/lib/percent.js.map +1 -0
- package/dist/dev/lib/read-px-property.js.map +1 -0
- package/dist/dev/lib/render.js +27 -0
- package/dist/dev/lib/render.js.map +1 -0
- package/dist/dev/lib/taper.js +36 -0
- package/dist/dev/lib/taper.js.map +1 -0
- package/dist/dev/lib/write-attribute.js.map +1 -0
- package/dist/skins/ivory.css +3 -3
- package/dist/skins/slate.css +4 -4
- package/dist/styles/controls.css +2 -1
- package/dist/styles/dial.css +70 -16
- package/dist/styles/key.css +71 -11
- package/dist/styles/led.css +75 -0
- package/dist/styles/material.css +154 -25
- package/dist/styles/meter.css +44 -8
- package/dist/styles/number.css +81 -0
- package/dist/styles/screen.css +6 -33
- package/dist/styles/segmented.css +45 -7
- package/dist/styles/slider.css +68 -4
- package/package.json +16 -13
- package/dist/define/dial.js.map +0 -1
- package/dist/define/key.js.map +0 -1
- package/dist/define/meter.js.map +0 -1
- package/dist/define/segmented.js.map +0 -1
- package/dist/define/slider.js.map +0 -1
- package/dist/elements/define-once.js.map +0 -1
- package/dist/elements/dial.js.map +0 -1
- package/dist/elements/key.d.ts +0 -17
- package/dist/elements/key.js.map +0 -1
- package/dist/elements/meter.js.map +0 -1
- package/dist/elements/range-element.d.ts +0 -32
- package/dist/elements/range-element.js.map +0 -1
- package/dist/elements/range-entry.js.map +0 -1
- package/dist/elements/segmented.js +0 -104
- package/dist/elements/segmented.js.map +0 -1
- package/dist/elements/slider.js.map +0 -1
- package/dist/elements/sonic-element.d.ts +0 -20
- package/dist/elements/sonic-element.js +0 -94
- package/dist/elements/sonic-element.js.map +0 -1
- package/dist/lib/read-px-property.js.map +0 -1
- package/dist/lib/render.js.map +0 -1
- package/dist/lib/taper.js.map +0 -1
- package/dist/lib/write-attribute.js.map +0 -1
- /package/dist/{define → dev/define}/dial.d.ts +0 -0
- /package/dist/{define → dev/define}/dial.js +0 -0
- /package/dist/{define → dev/define}/key.d.ts +0 -0
- /package/dist/{define → dev/define}/key.js +0 -0
- /package/dist/{define → dev/define}/meter.d.ts +0 -0
- /package/dist/{define → dev/define}/meter.js +0 -0
- /package/dist/{define → dev/define}/segmented.d.ts +0 -0
- /package/dist/{define → dev/define}/segmented.js +0 -0
- /package/dist/{define → dev/define}/slider.d.ts +0 -0
- /package/dist/{define → dev/define}/slider.js +0 -0
- /package/dist/{elements → dev/elements}/define-once.js +0 -0
- /package/dist/{lib → dev/lib}/read-px-property.js +0 -0
- /package/dist/{lib → dev/lib}/write-attribute.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { template } from "../lib/render.js";
|
|
2
|
+
import { SonicFormElement } from "./form-element.js";
|
|
3
|
+
import { copyNode } from "../lib/copy-node.js";
|
|
4
|
+
|
|
5
|
+
//#region src/elements/segmented.ts
|
|
6
|
+
const renderSegmented = template(`<div class="sonic-segmented" role="radiogroup"></div>`, HTMLDivElement);
|
|
7
|
+
const renderSegment = template(`
|
|
8
|
+
<button class="sonic-segmented-segment" role="radio" type="button">
|
|
9
|
+
<span class="sonic-segmented-cap"></span>
|
|
10
|
+
</button>
|
|
11
|
+
`, HTMLButtonElement);
|
|
12
|
+
const keySteps = /* @__PURE__ */ new Map([
|
|
13
|
+
["ArrowDown", 1],
|
|
14
|
+
["ArrowLeft", -1],
|
|
15
|
+
["ArrowRight", 1],
|
|
16
|
+
["ArrowUp", -1]
|
|
17
|
+
]);
|
|
18
|
+
function isUnder(segment, x, y) {
|
|
19
|
+
const box = segment.getBoundingClientRect();
|
|
20
|
+
return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
|
|
21
|
+
}
|
|
22
|
+
function segmentValue(segment) {
|
|
23
|
+
return segment.querySelector("[data-sonic-value]")?.dataset.sonicValue;
|
|
24
|
+
}
|
|
25
|
+
var SonicSegmented = class extends SonicFormElement {
|
|
26
|
+
static observedAttributes = [...SonicFormElement.observedAttributes, "value"];
|
|
27
|
+
get value() {
|
|
28
|
+
return this.#value;
|
|
29
|
+
}
|
|
30
|
+
set value(next) {
|
|
31
|
+
if (next === this.#value) return;
|
|
32
|
+
this.#value = next;
|
|
33
|
+
this.render();
|
|
34
|
+
}
|
|
35
|
+
#press;
|
|
36
|
+
#segmented = renderSegmented();
|
|
37
|
+
#value = "";
|
|
38
|
+
attributeChangedCallback(name) {
|
|
39
|
+
if (name === "value") this.#value = this.getAttribute("value") ?? "";
|
|
40
|
+
this.render();
|
|
41
|
+
}
|
|
42
|
+
connect(signal) {
|
|
43
|
+
const segmented = this.#segmented;
|
|
44
|
+
this.upgradeProperties("value");
|
|
45
|
+
this.mirrorChildren({
|
|
46
|
+
control: segmented,
|
|
47
|
+
copy: (options) => {
|
|
48
|
+
this.#copyOptions(options);
|
|
49
|
+
},
|
|
50
|
+
isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]")
|
|
51
|
+
}, signal);
|
|
52
|
+
this.checkStyles(segmented, "segmented.css");
|
|
53
|
+
segmented.addEventListener("click", (event) => {
|
|
54
|
+
if (event instanceof PointerEvent && event.pointerType !== "") return;
|
|
55
|
+
const segment = this.#segmentOf(event.target);
|
|
56
|
+
if (segment) this.#select(segment);
|
|
57
|
+
}, { signal });
|
|
58
|
+
this.#bindPress(segmented, signal);
|
|
59
|
+
segmented.addEventListener("keydown", (event) => {
|
|
60
|
+
if (event.defaultPrevented) return;
|
|
61
|
+
const segment = this.#segmentOf(event.target);
|
|
62
|
+
const next = segment && this.#keyTarget(segment, event.key);
|
|
63
|
+
if (!next) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
this.#select(next);
|
|
66
|
+
next.focus();
|
|
67
|
+
}, { signal });
|
|
68
|
+
}
|
|
69
|
+
focusTarget() {
|
|
70
|
+
return this.#segments().find((segment) => segment.tabIndex === 0);
|
|
71
|
+
}
|
|
72
|
+
render() {
|
|
73
|
+
const segmented = this.#segmented;
|
|
74
|
+
const segments = this.#segments();
|
|
75
|
+
const checked = segments.find((segment) => segmentValue(segment) === this.#value);
|
|
76
|
+
const focusable = checked ?? segments[0];
|
|
77
|
+
for (const segment of segments) {
|
|
78
|
+
segment.disabled = this.isDisabled();
|
|
79
|
+
segment.setAttribute("aria-checked", String(segment === checked));
|
|
80
|
+
segment.tabIndex = segment === focusable ? 0 : -1;
|
|
81
|
+
}
|
|
82
|
+
this.forwardNaming(segmented, true);
|
|
83
|
+
this.writeFormValue(checked ? this.#value : null, this.#value);
|
|
84
|
+
}
|
|
85
|
+
restoreState(state) {
|
|
86
|
+
this.value = state;
|
|
87
|
+
}
|
|
88
|
+
#bindPress(segmented, signal) {
|
|
89
|
+
const isOwn = (event) => event.pointerId === this.#press?.pointerId;
|
|
90
|
+
const release = (event) => {
|
|
91
|
+
if (isOwn(event)) this.#hold(void 0);
|
|
92
|
+
};
|
|
93
|
+
segmented.addEventListener("pointerdown", (event) => {
|
|
94
|
+
const segment = this.#segmentOf(event.target);
|
|
95
|
+
if (!segment || event.button !== 0 || this.isDisabled()) return;
|
|
96
|
+
segmented.setPointerCapture(event.pointerId);
|
|
97
|
+
this.#hold({
|
|
98
|
+
pointerId: event.pointerId,
|
|
99
|
+
segment
|
|
100
|
+
});
|
|
101
|
+
}, { signal });
|
|
102
|
+
segmented.addEventListener("pointermove", (event) => {
|
|
103
|
+
if (!isOwn(event)) return;
|
|
104
|
+
this.#hold({
|
|
105
|
+
pointerId: event.pointerId,
|
|
106
|
+
segment: this.#segments().find((segment) => isUnder(segment, event.clientX, event.clientY))
|
|
107
|
+
});
|
|
108
|
+
}, { signal });
|
|
109
|
+
segmented.addEventListener("pointerup", (event) => {
|
|
110
|
+
const segment = isOwn(event) ? this.#press?.segment : void 0;
|
|
111
|
+
release(event);
|
|
112
|
+
if (!segment || this.isDisabled()) return;
|
|
113
|
+
this.#select(segment);
|
|
114
|
+
if (segmented.matches(":focus-within")) segment.focus();
|
|
115
|
+
}, { signal });
|
|
116
|
+
segmented.addEventListener("pointercancel", release, { signal });
|
|
117
|
+
segmented.addEventListener("lostpointercapture", release, { signal });
|
|
118
|
+
signal.addEventListener("abort", () => {
|
|
119
|
+
this.#hold(void 0);
|
|
120
|
+
}, { once: true });
|
|
121
|
+
}
|
|
122
|
+
#copyOptions(options) {
|
|
123
|
+
const segmented = this.#segmented;
|
|
124
|
+
const segments = this.#segments();
|
|
125
|
+
const focused = this.#segmentOf(segmented.querySelector(":scope > :focus"));
|
|
126
|
+
const focusedValue = focused && segmentValue(focused);
|
|
127
|
+
for (const [index, option] of options.entries()) {
|
|
128
|
+
let segment = segments[index];
|
|
129
|
+
if (!segment) {
|
|
130
|
+
segment = renderSegment();
|
|
131
|
+
segmented.append(segment);
|
|
132
|
+
}
|
|
133
|
+
segment.firstElementChild?.replaceChildren(copyNode(option));
|
|
134
|
+
}
|
|
135
|
+
for (const segment of segments.slice(options.length)) segment.remove();
|
|
136
|
+
this.render();
|
|
137
|
+
if (focused) this.#refocus(focused, focusedValue);
|
|
138
|
+
}
|
|
139
|
+
#hold(press) {
|
|
140
|
+
this.#press = press;
|
|
141
|
+
for (const segment of this.#segments()) segment.toggleAttribute("data-sonic-pressed", segment === press?.segment);
|
|
142
|
+
}
|
|
143
|
+
#keyTarget(segment, key) {
|
|
144
|
+
const segments = this.#segments();
|
|
145
|
+
if (key === "Home") return segments[0];
|
|
146
|
+
if (key === "End") return segments.at(-1);
|
|
147
|
+
const step = keySteps.get(key);
|
|
148
|
+
if (step === void 0) return void 0;
|
|
149
|
+
return segments[(segments.indexOf(segment) + step + segments.length) % segments.length];
|
|
150
|
+
}
|
|
151
|
+
#refocus(focused, value) {
|
|
152
|
+
const segments = this.#segments();
|
|
153
|
+
const next = segments.find((segment) => segmentValue(segment) === value) ?? segments.find((segment) => segment.tabIndex === 0);
|
|
154
|
+
if (next !== focused) next?.focus();
|
|
155
|
+
}
|
|
156
|
+
#segmentOf(target) {
|
|
157
|
+
if (!(target instanceof Element)) return void 0;
|
|
158
|
+
const segment = target.closest(".sonic-segmented-segment");
|
|
159
|
+
return segment instanceof HTMLButtonElement && segment.parentElement === this.#segmented ? segment : void 0;
|
|
160
|
+
}
|
|
161
|
+
#segments() {
|
|
162
|
+
return [...this.#segmented.children].filter((child) => child instanceof HTMLButtonElement);
|
|
163
|
+
}
|
|
164
|
+
#select(segment) {
|
|
165
|
+
const next = segmentValue(segment);
|
|
166
|
+
if (next === void 0 || next === this.#value) return;
|
|
167
|
+
this.value = next;
|
|
168
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
export { SonicSegmented };
|
|
174
|
+
//# sourceMappingURL=segmented.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segmented.js","names":[],"sources":["../../../src/elements/segmented.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-segmented': SonicSegmented;\n\t}\n}\n\nconst renderSegmented = template(\n\t/* HTML */ `<div class=\"sonic-segmented\" role=\"radiogroup\"></div>`,\n\tHTMLDivElement,\n);\n\nconst renderSegment = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-segmented-segment\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-segmented-cap\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\n// RTL is not mirrored\nconst keySteps = new Map([\n\t['ArrowDown', 1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', -1],\n]);\n\ninterface Press {\n\tpointerId: number;\n\tsegment: HTMLButtonElement | undefined;\n}\n\nfunction isUnder(segment: Element, x: number, y: number): boolean {\n\tconst box = segment.getBoundingClientRect();\n\n\treturn x >= box.left && x < box.right && y >= box.top && y < box.bottom;\n}\n\nfunction segmentValue(segment: Element): string | undefined {\n\treturn segment.querySelector<HTMLElement | SVGElement>('[data-sonic-value]')?.dataset.sonicValue;\n}\n\nexport class SonicSegmented extends SonicFormElement {\n\tstatic override readonly observedAttributes = [...SonicFormElement.observedAttributes, 'value'];\n\n\t// As on the range, the property never writes the `value` attribute back\n\tget value(): string {\n\t\treturn this.#value;\n\t}\n\n\tset value(next: string) {\n\t\tif (next === this.#value) return;\n\n\t\tthis.#value = next;\n\t\tthis.render();\n\t}\n\n\t#press: Press | undefined;\n\n\treadonly #segmented = renderSegmented();\n\n\t#value = '';\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'value') this.#value = this.getAttribute('value') ?? '';\n\t\tthis.render();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst segmented = this.#segmented;\n\n\t\tthis.upgradeProperties('value');\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: segmented,\n\t\t\t\tcopy: (options) => {\n\t\t\t\t\tthis.#copyOptions(options);\n\t\t\t\t},\n\t\t\t\tisCopied: (child) => child instanceof Element && child.matches('[data-sonic-value]'),\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.checkStyles(segmented, 'segmented.css');\n\n\t\tsegmented.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (event instanceof PointerEvent && event.pointerType !== '') return;\n\n\t\t\t\tconst segment = this.#segmentOf(event.target);\n\t\t\t\tif (segment) this.#select(segment);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindPress(segmented, signal);\n\t\tsegmented.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.defaultPrevented) return;\n\n\t\t\t\tconst segment = this.#segmentOf(event.target);\n\t\t\t\tconst next = segment && this.#keyTarget(segment, event.key);\n\t\t\t\tif (!next) return;\n\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.#select(next);\n\t\t\t\tnext.focus();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.#segments().find((segment) => segment.tabIndex === 0);\n\t}\n\n\t// As a radio group, only a checked option submits\n\tprotected render(): void {\n\t\tconst segmented = this.#segmented;\n\t\tconst segments = this.#segments();\n\t\tconst checked = segments.find((segment) => segmentValue(segment) === this.#value);\n\t\tconst focusable = checked ?? segments[0];\n\n\t\tfor (const segment of segments) {\n\t\t\tsegment.disabled = this.isDisabled();\n\t\t\tsegment.setAttribute('aria-checked', String(segment === checked));\n\t\t\tsegment.tabIndex = segment === focusable ? 0 : -1;\n\t\t}\n\t\tthis.forwardNaming(segmented, true);\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(checked ? this.#value : null, this.#value);\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tthis.value = state;\n\t}\n\n\t#bindPress(segmented: HTMLElement, signal: AbortSignal): void {\n\t\tconst isOwn = (event: PointerEvent): boolean => event.pointerId === this.#press?.pointerId;\n\t\tconst release = (event: PointerEvent): void => {\n\t\t\tif (isOwn(event)) this.#hold(undefined);\n\t\t};\n\n\t\tsegmented.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tconst segment = this.#segmentOf(event.target);\n\t\t\t\tif (!segment || event.button !== 0 || this.isDisabled()) return;\n\n\t\t\t\tsegmented.setPointerCapture(event.pointerId);\n\t\t\t\tthis.#hold({ pointerId: event.pointerId, segment });\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsegmented.addEventListener(\n\t\t\t'pointermove',\n\t\t\t(event) => {\n\t\t\t\tif (!isOwn(event)) return;\n\n\t\t\t\tthis.#hold({\n\t\t\t\t\tpointerId: event.pointerId,\n\t\t\t\t\tsegment: this.#segments().find((segment) =>\n\t\t\t\t\t\tisUnder(segment, event.clientX, event.clientY),\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsegmented.addEventListener(\n\t\t\t'pointerup',\n\t\t\t(event) => {\n\t\t\t\tconst segment = isOwn(event) ? this.#press?.segment : undefined;\n\n\t\t\t\trelease(event);\n\t\t\t\tif (!segment || this.isDisabled()) return;\n\n\t\t\t\tthis.#select(segment);\n\t\t\t\tif (segmented.matches(':focus-within')) segment.focus();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsegmented.addEventListener('pointercancel', release, { signal });\n\t\tsegmented.addEventListener('lostpointercapture', release, { signal });\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tthis.#hold(undefined);\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t// Segments are kept by position, so focus follows its option's value across a reorder\n\t#copyOptions(options: Array<Node>): void {\n\t\tconst segmented = this.#segmented;\n\t\tconst segments = this.#segments();\n\t\tconst focused = this.#segmentOf(segmented.querySelector(':scope > :focus'));\n\t\tconst focusedValue = focused && segmentValue(focused);\n\n\t\tfor (const [index, option] of options.entries()) {\n\t\t\tlet segment = segments[index];\n\t\t\tif (!segment) {\n\t\t\t\tsegment = renderSegment();\n\t\t\t\tsegmented.append(segment);\n\t\t\t}\n\n\t\t\tsegment.firstElementChild?.replaceChildren(copyNode(option));\n\t\t}\n\t\tfor (const segment of segments.slice(options.length)) segment.remove();\n\t\tthis.render();\n\t\tif (focused) this.#refocus(focused, focusedValue);\n\t}\n\n\t#hold(press: Press | undefined): void {\n\t\tthis.#press = press;\n\t\tfor (const segment of this.#segments()) {\n\t\t\tsegment.toggleAttribute('data-sonic-pressed', segment === press?.segment);\n\t\t}\n\t}\n\n\t#keyTarget(segment: HTMLButtonElement, key: string): HTMLButtonElement | undefined {\n\t\tconst segments = this.#segments();\n\n\t\tif (key === 'Home') return segments[0];\n\t\tif (key === 'End') return segments.at(-1);\n\n\t\tconst step = keySteps.get(key);\n\t\tif (step === undefined) return undefined;\n\n\t\tconst index = segments.indexOf(segment) + step;\n\n\t\treturn segments[(index + segments.length) % segments.length];\n\t}\n\n\t#refocus(focused: HTMLButtonElement, value: string | undefined): void {\n\t\tconst segments = this.#segments();\n\t\tconst next =\n\t\t\tsegments.find((segment) => segmentValue(segment) === value) ??\n\t\t\tsegments.find((segment) => segment.tabIndex === 0);\n\n\t\tif (next !== focused) next?.focus();\n\t}\n\n\t#segmentOf(target: EventTarget | null): HTMLButtonElement | undefined {\n\t\tif (!(target instanceof Element)) return undefined;\n\n\t\tconst segment = target.closest('.sonic-segmented-segment');\n\n\t\treturn segment instanceof HTMLButtonElement && segment.parentElement === this.#segmented\n\t\t\t? segment\n\t\t\t: undefined;\n\t}\n\n\t#segments(): Array<HTMLButtonElement> {\n\t\treturn [...this.#segmented.children].filter(\n\t\t\t(child): child is HTMLButtonElement => child instanceof HTMLButtonElement,\n\t\t);\n\t}\n\n\t#select(segment: HTMLButtonElement): void {\n\t\tconst next = segmentValue(segment);\n\t\tif (next === undefined || next === this.#value) return;\n\n\t\tthis.value = next;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,kBAAkB,SACZ,yDACX,cACD;AAEA,MAAM,gBAAgB,SACV;;;;IAKX,iBACD;AAGA,MAAM,2BAAW,IAAI,IAAI;CACxB,CAAC,aAAa,CAAC;CACf,CAAC,aAAa,EAAE;CAChB,CAAC,cAAc,CAAC;CAChB,CAAC,WAAW,EAAE;AACf,CAAC;AAOD,SAAS,QAAQ,SAAkB,GAAW,GAAoB;CACjE,MAAM,MAAM,QAAQ,sBAAsB;CAE1C,OAAO,KAAK,IAAI,QAAQ,IAAI,IAAI,SAAS,KAAK,IAAI,OAAO,IAAI,IAAI;AAClE;AAEA,SAAS,aAAa,SAAsC;CAC3D,OAAO,QAAQ,cAAwC,oBAAoB,CAAC,EAAE,QAAQ;AACvF;AAEA,IAAa,iBAAb,cAAoC,iBAAiB;CACpD,OAAyB,qBAAqB,CAAC,GAAG,iBAAiB,oBAAoB,OAAO;CAG9F,IAAI,QAAgB;EACnB,OAAO,KAAK;CACb;CAEA,IAAI,MAAM,MAAc;EACvB,IAAI,SAAS,KAAK,QAAQ;EAE1B,KAAK,SAAS;EACd,KAAK,OAAO;CACb;CAEA;CAEA,AAAS,aAAa,gBAAgB;CAEtC,SAAS;CAET,yBAAyB,MAAoB;EAC5C,IAAI,SAAS,SAAS,KAAK,SAAS,KAAK,aAAa,OAAO,KAAK;EAClE,KAAK,OAAO;CACb;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,YAAY,KAAK;EAEvB,KAAK,kBAAkB,OAAO;EAC9B,KAAK,eACJ;GACC,SAAS;GACT,OAAO,YAAY;IAClB,KAAK,aAAa,OAAO;GAC1B;GACA,WAAW,UAAU,iBAAiB,WAAW,MAAM,QAAQ,oBAAoB;EACpF,GACA,MACD;EACA,KAAK,YAAY,WAAW,eAAe;EAE3C,UAAU,iBACT,UACC,UAAU;GACV,IAAI,iBAAiB,gBAAgB,MAAM,gBAAgB,IAAI;GAE/D,MAAM,UAAU,KAAK,WAAW,MAAM,MAAM;GAC5C,IAAI,SAAS,KAAK,QAAQ,OAAO;EAClC,GACA,EAAE,OAAO,CACV;EACA,KAAK,WAAW,WAAW,MAAM;EACjC,UAAU,iBACT,YACC,UAAU;GACV,IAAI,MAAM,kBAAkB;GAE5B,MAAM,UAAU,KAAK,WAAW,MAAM,MAAM;GAC5C,MAAM,OAAO,WAAW,KAAK,WAAW,SAAS,MAAM,GAAG;GAC1D,IAAI,CAAC,MAAM;GAEX,MAAM,eAAe;GACrB,KAAK,QAAQ,IAAI;GACjB,KAAK,MAAM;EACZ,GACA,EAAE,OAAO,CACV;CACD;CAEA,AAAmB,cAAuC;EACzD,OAAO,KAAK,UAAU,CAAC,CAAC,MAAM,YAAY,QAAQ,aAAa,CAAC;CACjE;CAGA,AAAU,SAAe;EACxB,MAAM,YAAY,KAAK;EACvB,MAAM,WAAW,KAAK,UAAU;EAChC,MAAM,UAAU,SAAS,MAAM,YAAY,aAAa,OAAO,MAAM,KAAK,MAAM;EAChF,MAAM,YAAY,WAAW,SAAS;EAEtC,KAAK,MAAM,WAAW,UAAU;GAC/B,QAAQ,WAAW,KAAK,WAAW;GACnC,QAAQ,aAAa,gBAAgB,OAAO,YAAY,OAAO,CAAC;GAChE,QAAQ,WAAW,YAAY,YAAY,IAAI;EAChD;EACA,KAAK,cAAc,WAAW,IAAI;EAElC,KAAK,eAAe,UAAU,KAAK,SAAS,MAAM,KAAK,MAAM;CAC9D;CAEA,AAAU,aAAa,OAAqB;EAC3C,KAAK,QAAQ;CACd;CAEA,WAAW,WAAwB,QAA2B;EAC7D,MAAM,SAAS,UAAiC,MAAM,cAAc,KAAK,QAAQ;EACjF,MAAM,WAAW,UAA8B;GAC9C,IAAI,MAAM,KAAK,GAAG,KAAK,MAAM,MAAS;EACvC;EAEA,UAAU,iBACT,gBACC,UAAU;GACV,MAAM,UAAU,KAAK,WAAW,MAAM,MAAM;GAC5C,IAAI,CAAC,WAAW,MAAM,WAAW,KAAK,KAAK,WAAW,GAAG;GAEzD,UAAU,kBAAkB,MAAM,SAAS;GAC3C,KAAK,MAAM;IAAE,WAAW,MAAM;IAAW;GAAQ,CAAC;EACnD,GACA,EAAE,OAAO,CACV;EACA,UAAU,iBACT,gBACC,UAAU;GACV,IAAI,CAAC,MAAM,KAAK,GAAG;GAEnB,KAAK,MAAM;IACV,WAAW,MAAM;IACjB,SAAS,KAAK,UAAU,CAAC,CAAC,MAAM,YAC/B,QAAQ,SAAS,MAAM,SAAS,MAAM,OAAO,CAC9C;GACD,CAAC;EACF,GACA,EAAE,OAAO,CACV;EACA,UAAU,iBACT,cACC,UAAU;GACV,MAAM,UAAU,MAAM,KAAK,IAAI,KAAK,QAAQ,UAAU;GAEtD,QAAQ,KAAK;GACb,IAAI,CAAC,WAAW,KAAK,WAAW,GAAG;GAEnC,KAAK,QAAQ,OAAO;GACpB,IAAI,UAAU,QAAQ,eAAe,GAAG,QAAQ,MAAM;EACvD,GACA,EAAE,OAAO,CACV;EACA,UAAU,iBAAiB,iBAAiB,SAAS,EAAE,OAAO,CAAC;EAC/D,UAAU,iBAAiB,sBAAsB,SAAS,EAAE,OAAO,CAAC;EACpE,OAAO,iBACN,eACM;GACL,KAAK,MAAM,MAAS;EACrB,GACA,EAAE,MAAM,KAAK,CACd;CACD;CAGA,aAAa,SAA4B;EACxC,MAAM,YAAY,KAAK;EACvB,MAAM,WAAW,KAAK,UAAU;EAChC,MAAM,UAAU,KAAK,WAAW,UAAU,cAAc,iBAAiB,CAAC;EAC1E,MAAM,eAAe,WAAW,aAAa,OAAO;EAEpD,KAAK,MAAM,CAAC,OAAO,WAAW,QAAQ,QAAQ,GAAG;GAChD,IAAI,UAAU,SAAS;GACvB,IAAI,CAAC,SAAS;IACb,UAAU,cAAc;IACxB,UAAU,OAAO,OAAO;GACzB;GAEA,QAAQ,mBAAmB,gBAAgB,SAAS,MAAM,CAAC;EAC5D;EACA,KAAK,MAAM,WAAW,SAAS,MAAM,QAAQ,MAAM,GAAG,QAAQ,OAAO;EACrE,KAAK,OAAO;EACZ,IAAI,SAAS,KAAK,SAAS,SAAS,YAAY;CACjD;CAEA,MAAM,OAAgC;EACrC,KAAK,SAAS;EACd,KAAK,MAAM,WAAW,KAAK,UAAU,GACpC,QAAQ,gBAAgB,sBAAsB,YAAY,OAAO,OAAO;CAE1E;CAEA,WAAW,SAA4B,KAA4C;EAClF,MAAM,WAAW,KAAK,UAAU;EAEhC,IAAI,QAAQ,QAAQ,OAAO,SAAS;EACpC,IAAI,QAAQ,OAAO,OAAO,SAAS,GAAG,EAAE;EAExC,MAAM,OAAO,SAAS,IAAI,GAAG;EAC7B,IAAI,SAAS,QAAW,OAAO;EAI/B,OAAO,UAFO,SAAS,QAAQ,OAAO,IAAI,OAEjB,SAAS,UAAU,SAAS;CACtD;CAEA,SAAS,SAA4B,OAAiC;EACrE,MAAM,WAAW,KAAK,UAAU;EAChC,MAAM,OACL,SAAS,MAAM,YAAY,aAAa,OAAO,MAAM,KAAK,KAC1D,SAAS,MAAM,YAAY,QAAQ,aAAa,CAAC;EAElD,IAAI,SAAS,SAAS,MAAM,MAAM;CACnC;CAEA,WAAW,QAA2D;EACrE,IAAI,EAAE,kBAAkB,UAAU,OAAO;EAEzC,MAAM,UAAU,OAAO,QAAQ,0BAA0B;EAEzD,OAAO,mBAAmB,qBAAqB,QAAQ,kBAAkB,KAAK,aAC3E,UACA;CACJ;CAEA,YAAsC;EACrC,OAAO,CAAC,GAAG,KAAK,WAAW,QAAQ,CAAC,CAAC,QACnC,UAAsC,iBAAiB,iBACzD;CACD;CAEA,QAAQ,SAAkC;EACzC,MAAM,OAAO,aAAa,OAAO;EACjC,IAAI,SAAS,UAAa,SAAS,KAAK,QAAQ;EAEhD,KAAK,QAAQ;EACb,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;CAC1D;AACD"}
|
|
@@ -8,7 +8,15 @@ declare global {
|
|
|
8
8
|
export declare class SonicSlider extends SonicRangeElement {
|
|
9
9
|
#private;
|
|
10
10
|
static readonly observedAttributes: string[];
|
|
11
|
+
get groovePress(): 'jump' | 'none';
|
|
12
|
+
set groovePress(gesture: 'jump' | 'none' | undefined);
|
|
13
|
+
get modulation(): number;
|
|
14
|
+
set modulation(amount: number | undefined);
|
|
15
|
+
get orientation(): 'horizontal' | 'vertical';
|
|
16
|
+
set orientation(direction: 'horizontal' | 'vertical' | undefined);
|
|
17
|
+
connectedCallback(): void;
|
|
11
18
|
protected connect(signal: AbortSignal): void;
|
|
19
|
+
protected focusTarget(): HTMLElement;
|
|
12
20
|
protected render(): void;
|
|
13
21
|
}
|
|
14
22
|
//#endregion
|
|
@@ -6,6 +6,7 @@ const renderSlider = template(`
|
|
|
6
6
|
<div class="sonic-slider" role="slider" tabindex="0">
|
|
7
7
|
<div class="sonic-slider-marks"></div>
|
|
8
8
|
<div class="sonic-slider-groove"></div>
|
|
9
|
+
<div class="sonic-slider-modulation"></div>
|
|
9
10
|
<div class="sonic-slider-cap"></div>
|
|
10
11
|
<div class="sonic-slider-readout" popover="manual">
|
|
11
12
|
<span></span>
|
|
@@ -21,8 +22,34 @@ const renderSlider = template(`
|
|
|
21
22
|
</div>
|
|
22
23
|
`, HTMLDivElement);
|
|
23
24
|
var SonicSlider = class extends SonicRangeElement {
|
|
24
|
-
static observedAttributes = [
|
|
25
|
+
static observedAttributes = [
|
|
26
|
+
...SonicRangeElement.observedAttributes,
|
|
27
|
+
"modulation",
|
|
28
|
+
"orientation"
|
|
29
|
+
];
|
|
30
|
+
get groovePress() {
|
|
31
|
+
return this.getAttribute("groove-press") === "none" ? "none" : "jump";
|
|
32
|
+
}
|
|
33
|
+
set groovePress(gesture) {
|
|
34
|
+
this.reflect("groove-press", gesture);
|
|
35
|
+
}
|
|
36
|
+
get modulation() {
|
|
37
|
+
return this.numberAttribute("modulation", 0);
|
|
38
|
+
}
|
|
39
|
+
set modulation(amount) {
|
|
40
|
+
this.reflect("modulation", amount);
|
|
41
|
+
}
|
|
42
|
+
get orientation() {
|
|
43
|
+
return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
|
|
44
|
+
}
|
|
45
|
+
set orientation(direction) {
|
|
46
|
+
this.reflect("orientation", direction);
|
|
47
|
+
}
|
|
25
48
|
#slider = renderSlider();
|
|
49
|
+
connectedCallback() {
|
|
50
|
+
this.upgradeProperties("groovePress", "modulation", "orientation");
|
|
51
|
+
super.connectedCallback();
|
|
52
|
+
}
|
|
26
53
|
connect(signal) {
|
|
27
54
|
const slider = this.#slider;
|
|
28
55
|
const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
|
|
@@ -32,24 +59,30 @@ var SonicSlider = class extends SonicRangeElement {
|
|
|
32
59
|
this.bindGestures(slider, signal, (event) => {
|
|
33
60
|
const axis = this.#axis(slider, cap);
|
|
34
61
|
if (event.target instanceof Node && cap.contains(event.target)) return axis;
|
|
35
|
-
if (this.
|
|
62
|
+
if (this.groovePress === "none") return;
|
|
36
63
|
this.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));
|
|
37
64
|
return axis;
|
|
38
65
|
});
|
|
39
66
|
}
|
|
67
|
+
focusTarget() {
|
|
68
|
+
return this.#slider;
|
|
69
|
+
}
|
|
40
70
|
render() {
|
|
41
71
|
const slider = this.#slider;
|
|
42
72
|
const positions = this.positions();
|
|
73
|
+
const [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);
|
|
43
74
|
slider.style.setProperty("--_sonic-slider-value", String(this.fraction(this.value)));
|
|
44
75
|
slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
|
|
76
|
+
slider.style.setProperty("--_sonic-slider-modulation-from", String(modulationFrom));
|
|
77
|
+
slider.style.setProperty("--_sonic-slider-modulation-to", String(modulationTo));
|
|
45
78
|
if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
|
|
46
79
|
else slider.style.setProperty("--_sonic-slider-positions", String(positions));
|
|
47
|
-
this.renderAria(slider, this
|
|
80
|
+
this.renderAria(slider, this.orientation);
|
|
48
81
|
}
|
|
49
82
|
#axis(slider, cap) {
|
|
50
83
|
const track = slider.getBoundingClientRect();
|
|
51
84
|
const capBox = cap.getBoundingClientRect();
|
|
52
|
-
if (this
|
|
85
|
+
if (this.orientation === "vertical") return {
|
|
53
86
|
position: (event) => -event.clientY,
|
|
54
87
|
startPx: capBox.height / 2 - track.bottom,
|
|
55
88
|
travelPx: Math.max(1, track.height - capBox.height)
|
|
@@ -60,9 +93,6 @@ var SonicSlider = class extends SonicRangeElement {
|
|
|
60
93
|
travelPx: Math.max(1, track.width - capBox.width)
|
|
61
94
|
};
|
|
62
95
|
}
|
|
63
|
-
#isVertical() {
|
|
64
|
-
return this.getAttribute("orientation") === "vertical";
|
|
65
|
-
}
|
|
66
96
|
};
|
|
67
97
|
|
|
68
98
|
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slider.js","names":[],"sources":["../../../src/elements/slider.ts"],"sourcesContent":["import type { RangeAxis } from '#elements/range-element.ts';\n\nimport { SonicRangeElement } from '#elements/range-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-slider': SonicSlider;\n\t}\n}\n\ninterface SliderAxis extends RangeAxis {\n\t// The cap's centre at the minimum, on the same scale as `position`\n\tstartPx: number;\n}\n\nconst renderSlider = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-slider\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-slider-marks\"></div>\n\t\t\t<div class=\"sonic-slider-groove\"></div>\n\t\t\t<div class=\"sonic-slider-modulation\"></div>\n\t\t\t<div class=\"sonic-slider-cap\"></div>\n\t\t\t<div class=\"sonic-slider-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-slider-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicSlider extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'modulation',\n\t\t'orientation',\n\t];\n\n\t// Named for the absent attribute, as `input.type` reads `text`\n\tget groovePress(): 'jump' | 'none' {\n\t\treturn this.getAttribute('groove-press') === 'none' ? 'none' : 'jump';\n\t}\n\n\tset groovePress(gesture: 'jump' | 'none' | undefined) {\n\t\tthis.reflect('groove-press', gesture);\n\t}\n\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\treadonly #slider = renderSlider();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('groovePress', 'modulation', 'orientation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst slider = this.#slider;\n\t\tconst cap = requireChild(slider, '.sonic-slider-cap', HTMLDivElement);\n\n\t\tthis.appendOnce(slider);\n\t\tthis.render();\n\t\tthis.checkStyles(slider, 'slider.css');\n\t\tthis.bindGestures(slider, signal, (event) => {\n\t\t\tconst axis = this.#axis(slider, cap);\n\t\t\tif (event.target instanceof Node && cap.contains(event.target)) return axis;\n\n\t\t\tif (this.groovePress === 'none') return;\n\n\t\t\tthis.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));\n\n\t\t\treturn axis;\n\t\t});\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#slider;\n\t}\n\n\tprotected render(): void {\n\t\tconst slider = this.#slider;\n\t\tconst positions = this.positions();\n\t\tconst [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);\n\n\t\tslider.style.setProperty('--_sonic-slider-value', String(this.fraction(this.value)));\n\t\tslider.style.setProperty('--_sonic-slider-origin', String(this.originFraction()));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-from', String(modulationFrom));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-to', String(modulationTo));\n\t\tif (positions === undefined) slider.style.removeProperty('--_sonic-slider-positions');\n\t\telse slider.style.setProperty('--_sonic-slider-positions', String(positions));\n\t\tthis.renderAria(slider, this.orientation);\n\t}\n\n\t#axis(slider: HTMLElement, cap: HTMLElement): SliderAxis {\n\t\tconst track = slider.getBoundingClientRect();\n\t\tconst capBox = cap.getBoundingClientRect();\n\n\t\tif (this.orientation === 'vertical') {\n\t\t\treturn {\n\t\t\t\tposition: (event) => -event.clientY,\n\t\t\t\tstartPx: capBox.height / 2 - track.bottom,\n\t\t\t\ttravelPx: Math.max(1, track.height - capBox.height),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tposition: (event) => event.clientX,\n\t\t\tstartPx: track.left + capBox.width / 2,\n\t\t\ttravelPx: Math.max(1, track.width - capBox.width),\n\t\t};\n\t}\n}\n"],"mappings":";;;;AAgBA,MAAM,eAAe,SACT;;;;;;;;;;;;;;;;;;IAmBX,cACD;AAEA,IAAa,cAAb,cAAiC,kBAAkB;CAClD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAGA,IAAI,cAA+B;EAClC,OAAO,KAAK,aAAa,cAAc,MAAM,SAAS,SAAS;CAChE;CAEA,IAAI,YAAY,SAAsC;EACrD,KAAK,QAAQ,gBAAgB,OAAO;CACrC;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAEA,IAAI,cAAyC;EAC5C,OAAO,KAAK,aAAa,aAAa,MAAM,aAAa,aAAa;CACvE;CAEA,IAAI,YAAY,WAAkD;EACjE,KAAK,QAAQ,eAAe,SAAS;CACtC;CAEA,AAAS,UAAU,aAAa;CAEhC,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,eAAe,cAAc,aAAa;EACjE,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EACpB,MAAM,MAAM,aAAa,QAAQ,qBAAqB,cAAc;EAEpE,KAAK,WAAW,MAAM;EACtB,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,YAAY;EACrC,KAAK,aAAa,QAAQ,SAAS,UAAU;GAC5C,MAAM,OAAO,KAAK,MAAM,QAAQ,GAAG;GACnC,IAAI,MAAM,kBAAkB,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,OAAO;GAEvE,IAAI,KAAK,gBAAgB,QAAQ;GAEjC,KAAK,MAAM,KAAK,SAAS,KAAK,SAAS,KAAK,IAAI,KAAK,WAAW,KAAK,QAAQ,CAAC;GAE9E,OAAO;EACR,CAAC;CACF;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,SAAS,KAAK;EACpB,MAAM,YAAY,KAAK,UAAU;EACjC,MAAM,CAAC,gBAAgB,gBAAgB,KAAK,oBAAoB,KAAK,UAAU;EAE/E,OAAO,MAAM,YAAY,yBAAyB,OAAO,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;EACnF,OAAO,MAAM,YAAY,0BAA0B,OAAO,KAAK,eAAe,CAAC,CAAC;EAChF,OAAO,MAAM,YAAY,mCAAmC,OAAO,cAAc,CAAC;EAClF,OAAO,MAAM,YAAY,iCAAiC,OAAO,YAAY,CAAC;EAC9E,IAAI,cAAc,QAAW,OAAO,MAAM,eAAe,2BAA2B;OAC/E,OAAO,MAAM,YAAY,6BAA6B,OAAO,SAAS,CAAC;EAC5E,KAAK,WAAW,QAAQ,KAAK,WAAW;CACzC;CAEA,MAAM,QAAqB,KAA8B;EACxD,MAAM,QAAQ,OAAO,sBAAsB;EAC3C,MAAM,SAAS,IAAI,sBAAsB;EAEzC,IAAI,KAAK,gBAAgB,YACxB,OAAO;GACN,WAAW,UAAU,CAAC,MAAM;GAC5B,SAAS,OAAO,SAAS,IAAI,MAAM;GACnC,UAAU,KAAK,IAAI,GAAG,MAAM,SAAS,OAAO,MAAM;EACnD;EAGD,OAAO;GACN,WAAW,UAAU,MAAM;GAC3B,SAAS,MAAM,OAAO,OAAO,QAAQ;GACrC,UAAU,KAAK,IAAI,GAAG,MAAM,QAAQ,OAAO,KAAK;EACjD;CACD;AACD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/elements/sonic-element.d.ts
|
|
2
|
+
interface Mirror {
|
|
3
|
+
control: Element;
|
|
4
|
+
copy: (originals: Array<ChildNode>) => void;
|
|
5
|
+
isCopied?: (child: Node) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare abstract class SonicElement extends HTMLElement {
|
|
8
|
+
#private;
|
|
9
|
+
static readonly observedAttributes: string[];
|
|
10
|
+
get disabled(): boolean;
|
|
11
|
+
set disabled(isDisabled: boolean);
|
|
12
|
+
protected readonly internals: ElementInternals | undefined;
|
|
13
|
+
blur(): void;
|
|
14
|
+
connectedCallback(): void;
|
|
15
|
+
connectedMoveCallback(): void;
|
|
16
|
+
disconnectedCallback(): void;
|
|
17
|
+
focus(options?: FocusOptions): void;
|
|
18
|
+
protected appendOnce(child: Element): void;
|
|
19
|
+
protected checkStyles(control: HTMLElement, sheet: string): void;
|
|
20
|
+
protected abstract connect(signal: AbortSignal): void;
|
|
21
|
+
protected focusTarget(): HTMLElement | undefined;
|
|
22
|
+
protected forwardNaming(target: Element, isNamed: boolean): void;
|
|
23
|
+
protected isDisabled(): boolean;
|
|
24
|
+
protected mirrorChildren({ control, copy, isCopied }: Mirror, signal: AbortSignal): void;
|
|
25
|
+
protected namingLabels(): Array<Element>;
|
|
26
|
+
protected numberAttribute(name: string, fallback: number): number;
|
|
27
|
+
protected optionalNumberAttribute(name: string): number | undefined;
|
|
28
|
+
protected reflect(name: string, value: boolean | null | number | string | undefined): void;
|
|
29
|
+
protected toggleState(state: string, isOn: boolean): void;
|
|
30
|
+
protected upgradeProperties(...names: Array<keyof this>): void;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//# sourceMappingURL=sonic-element.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { attachSlot } from "../lib/render.js";
|
|
2
|
+
import { writeAttribute } from "../lib/write-attribute.js";
|
|
3
|
+
|
|
4
|
+
//#region src/elements/sonic-element.ts
|
|
5
|
+
const namingAttributes = [
|
|
6
|
+
"aria-describedby",
|
|
7
|
+
"aria-label",
|
|
8
|
+
"aria-labelledby"
|
|
9
|
+
];
|
|
10
|
+
const checkedClasses = /* @__PURE__ */ new WeakSet();
|
|
11
|
+
function writeLabelledBy(target, labels) {
|
|
12
|
+
const current = target.ariaLabelledByElements;
|
|
13
|
+
if (current?.length === labels.length && labels.every((label, index) => label === current[index])) return;
|
|
14
|
+
target.ariaLabelledByElements = labels;
|
|
15
|
+
}
|
|
16
|
+
var SonicElement = class extends HTMLElement {
|
|
17
|
+
static observedAttributes = [...namingAttributes, "disabled"];
|
|
18
|
+
get disabled() {
|
|
19
|
+
return this.hasAttribute("disabled");
|
|
20
|
+
}
|
|
21
|
+
set disabled(isDisabled) {
|
|
22
|
+
this.reflect("disabled", isDisabled);
|
|
23
|
+
}
|
|
24
|
+
internals = "attachInternals" in HTMLElement.prototype ? this.attachInternals() : void 0;
|
|
25
|
+
#connection;
|
|
26
|
+
#slot;
|
|
27
|
+
blur() {
|
|
28
|
+
this.#focused()?.blur();
|
|
29
|
+
}
|
|
30
|
+
connectedCallback() {
|
|
31
|
+
this.upgradeProperties("disabled");
|
|
32
|
+
if (this.#connection) return;
|
|
33
|
+
const connection = new AbortController();
|
|
34
|
+
try {
|
|
35
|
+
this.connect(connection.signal);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
connection.abort();
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
this.#connection = connection;
|
|
41
|
+
}
|
|
42
|
+
connectedMoveCallback() {}
|
|
43
|
+
disconnectedCallback() {
|
|
44
|
+
const connection = this.#connection;
|
|
45
|
+
if (!connection) return;
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (this.isConnected || this.#connection !== connection) return;
|
|
48
|
+
connection.abort();
|
|
49
|
+
this.#connection = void 0;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
focus(options) {
|
|
53
|
+
const target = this.focusTarget();
|
|
54
|
+
if (!target) {
|
|
55
|
+
super.focus(options);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!this.isDisabled() && !this.#focused()) target.focus(options);
|
|
59
|
+
}
|
|
60
|
+
appendOnce(child) {
|
|
61
|
+
if (child.parentNode !== this) this.append(child);
|
|
62
|
+
}
|
|
63
|
+
checkStyles(control, sheet) {
|
|
64
|
+
const elementClass = this.constructor;
|
|
65
|
+
if (checkedClasses.has(elementClass)) return;
|
|
66
|
+
checkedClasses.add(elementClass);
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
if (!control.isConnected) {
|
|
69
|
+
checkedClasses.delete(elementClass);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const styles = getComputedStyle(control);
|
|
73
|
+
const missing = [styles.getPropertyValue("--_sonic-unlit") === "" ? "material.css" : "", styles.getPropertyValue("--_sonic-unit") === "" ? sheet : ""].filter(Boolean);
|
|
74
|
+
if (missing.length === 0) return;
|
|
75
|
+
console.warn(`<${this.localName}> draws blank without ${missing.map((file) => `@xsynaptic/sonic-ui/${file}`).join(" and ")} (or controls.css)`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
focusTarget() {}
|
|
79
|
+
forwardNaming(target, isNamed) {
|
|
80
|
+
const labels = isNamed ? this.namingLabels() : [];
|
|
81
|
+
for (const name of namingAttributes) {
|
|
82
|
+
if (name === "aria-labelledby" && labels.length > 0) continue;
|
|
83
|
+
const value = this.getAttribute(name);
|
|
84
|
+
writeAttribute(target, name, isNamed && value !== null ? value : void 0);
|
|
85
|
+
}
|
|
86
|
+
if (labels.length > 0) writeLabelledBy(target, labels);
|
|
87
|
+
}
|
|
88
|
+
isDisabled() {
|
|
89
|
+
return this.disabled || this.matches(":disabled");
|
|
90
|
+
}
|
|
91
|
+
mirrorChildren({ control, copy, isCopied = () => true }, signal) {
|
|
92
|
+
const slot = this.#slot ?? attachSlot(this);
|
|
93
|
+
this.#slot = slot;
|
|
94
|
+
const mirror = () => {
|
|
95
|
+
const children = [...this.childNodes].filter((child) => child !== control);
|
|
96
|
+
copy(children.filter((child) => isCopied(child)));
|
|
97
|
+
this.appendOnce(control);
|
|
98
|
+
slot.assign(...children.filter((child) => (child instanceof Element || child instanceof Text) && !isCopied(child)), control);
|
|
99
|
+
};
|
|
100
|
+
const observer = new MutationObserver((records) => {
|
|
101
|
+
if (records.some((record) => this.#isCopiedChange(record, control, isCopied))) mirror();
|
|
102
|
+
});
|
|
103
|
+
mirror();
|
|
104
|
+
observer.observe(this, {
|
|
105
|
+
attributes: true,
|
|
106
|
+
characterData: true,
|
|
107
|
+
childList: true,
|
|
108
|
+
subtree: true
|
|
109
|
+
});
|
|
110
|
+
signal.addEventListener("abort", () => {
|
|
111
|
+
observer.disconnect();
|
|
112
|
+
}, { once: true });
|
|
113
|
+
}
|
|
114
|
+
namingLabels() {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
numberAttribute(name, fallback) {
|
|
118
|
+
const attribute = this.getAttribute(name);
|
|
119
|
+
if (attribute === null) return fallback;
|
|
120
|
+
const parsed = Number(attribute);
|
|
121
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
122
|
+
}
|
|
123
|
+
optionalNumberAttribute(name) {
|
|
124
|
+
const value = this.numberAttribute(name, NaN);
|
|
125
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
126
|
+
}
|
|
127
|
+
reflect(name, value) {
|
|
128
|
+
if (value === true) {
|
|
129
|
+
this.setAttribute(name, "");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
writeAttribute(this, name, value === false || value === null ? void 0 : value?.toString());
|
|
133
|
+
}
|
|
134
|
+
toggleState(state, isOn) {
|
|
135
|
+
if (isOn) this.internals?.states.add(state);
|
|
136
|
+
else this.internals?.states.delete(state);
|
|
137
|
+
}
|
|
138
|
+
upgradeProperties(...names) {
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
if (!Object.hasOwn(this, name)) continue;
|
|
141
|
+
const value = this[name];
|
|
142
|
+
Reflect.deleteProperty(this, name);
|
|
143
|
+
this[name] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
#childHolding(node) {
|
|
147
|
+
let current = node;
|
|
148
|
+
while (current && current.parentNode !== this) current = current.parentNode;
|
|
149
|
+
return current ?? void 0;
|
|
150
|
+
}
|
|
151
|
+
#focused() {
|
|
152
|
+
const root = this.getRootNode();
|
|
153
|
+
const active = root instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;
|
|
154
|
+
return active instanceof HTMLElement && this.contains(active) ? active : void 0;
|
|
155
|
+
}
|
|
156
|
+
#isCopiedChange(record, control, isCopied) {
|
|
157
|
+
if (record.target === this) {
|
|
158
|
+
if (record.type === "attributes") return false;
|
|
159
|
+
return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
|
|
160
|
+
}
|
|
161
|
+
const child = this.#childHolding(record.target);
|
|
162
|
+
if (child === void 0 || child === control) return false;
|
|
163
|
+
return isCopied(child) || record.type === "attributes" && record.target === child;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
export { SonicElement };
|
|
169
|
+
//# sourceMappingURL=sonic-element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sonic-element.js","names":[],"sources":["../../../src/elements/sonic-element.ts"],"sourcesContent":["import { attachSlot } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\n// Replaced at build: `true` in the `development` tree, `false` in the default one\n// eslint-disable-next-line unicorn/consistent-boolean-name -- the name bundlers and other kits use\ndeclare const __DEV__: boolean;\n\n// Forwarding another attribute is a public API decision\nconst namingAttributes = ['aria-describedby', 'aria-label', 'aria-labelledby'];\n\nconst checkedClasses = new WeakSet<object>();\n\n// Rewriting the same labels on every render would churn the attribute through a drag\nfunction writeLabelledBy(target: Element, labels: Array<Element>): void {\n\tconst current = target.ariaLabelledByElements;\n\tif (\n\t\tcurrent?.length === labels.length &&\n\t\tlabels.every((label, index) => label === current[index])\n\t) {\n\t\treturn;\n\t}\n\n\ttarget.ariaLabelledByElements = labels;\n}\n\ninterface Mirror {\n\tcontrol: Element;\n\tcopy: (originals: Array<ChildNode>) => void;\n\t// Children left out render beside the control, as they are\n\tisCopied?: (child: Node) => boolean;\n}\n\n// eslint-disable-next-line wc/define-tag-after-class-definition -- abstract, so only its subclasses are ever defined\nexport abstract class SonicElement extends HTMLElement {\n\tstatic readonly observedAttributes = [...namingAttributes, 'disabled'];\n\n\tget disabled(): boolean {\n\t\treturn this.hasAttribute('disabled');\n\t}\n\n\tset disabled(isDisabled: boolean) {\n\t\tthis.reflect('disabled', isDisabled);\n\t}\n\n\t// happy-dom has no `ElementInternals`\n\tprotected readonly internals =\n\t\t'attachInternals' in HTMLElement.prototype ? this.attachInternals() : undefined;\n\n\t#connection: AbortController | undefined;\n\n\t#slot: HTMLSlotElement | undefined;\n\n\toverride blur(): void {\n\t\tthis.#focused()?.blur();\n\t}\n\n\tconnectedCallback(): void {\n\t\tthis.upgradeProperties('disabled');\n\t\tif (this.#connection) return;\n\n\t\tconst connection = new AbortController();\n\n\t\t// A throw leaves nothing bound behind it\n\t\ttry {\n\t\t\tthis.connect(connection.signal);\n\t\t} catch (error) {\n\t\t\tconnection.abort();\n\t\t\tthrow error;\n\t\t}\n\n\t\tthis.#connection = connection;\n\t}\n\n\tconnectedMoveCallback(): void {\n\t\t// Defined so `moveBefore` from a persisting router keeps every binding\n\t}\n\n\t// Without `moveBefore` (Safari), a persisting router disconnects and reconnects in one task\n\tdisconnectedCallback(): void {\n\t\tconst connection = this.#connection;\n\t\tif (!connection) return;\n\n\t\tqueueMicrotask(() => {\n\t\t\tif (this.isConnected || this.#connection !== connection) return;\n\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t});\n\t}\n\n\t// As `delegatesFocus` does, focus already inside the control stays where it is\n\toverride focus(options?: FocusOptions): void {\n\t\tconst target = this.focusTarget();\n\t\tif (!target) {\n\t\t\tsuper.focus(options);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.isDisabled() && !this.#focused()) target.focus(options);\n\t}\n\n\tprotected appendOnce(child: Element): void {\n\t\tif (child.parentNode !== this) this.append(child);\n\t}\n\n\t// A missing sheet draws the control blank without an error\n\tprotected checkStyles(control: HTMLElement, sheet: string): void {\n\t\tif (!__DEV__) return;\n\n\t\tconst elementClass = this.constructor;\n\t\tif (checkedClasses.has(elementClass)) return;\n\n\t\tcheckedClasses.add(elementClass);\n\t\trequestAnimationFrame(() => {\n\t\t\t// A detached control computes no styles\n\t\t\tif (!control.isConnected) {\n\t\t\t\tcheckedClasses.delete(elementClass);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst styles = getComputedStyle(control);\n\t\t\tconst missing = [\n\t\t\t\tstyles.getPropertyValue('--_sonic-unlit') === '' ? 'material.css' : '',\n\t\t\t\tstyles.getPropertyValue('--_sonic-unit') === '' ? sheet : '',\n\t\t\t].filter(Boolean);\n\t\t\tif (missing.length === 0) return;\n\n\t\t\tconsole.warn(\n\t\t\t\t`<${this.localName}> draws blank without ${missing.map((file) => `@xsynaptic/sonic-ui/${file}`).join(' and ')} (or controls.css)`,\n\t\t\t);\n\t\t});\n\t}\n\n\tprotected abstract connect(signal: AbortSignal): void;\n\n\tprotected focusTarget(): HTMLElement | undefined {\n\t\treturn undefined;\n\t}\n\n\tprotected forwardNaming(target: Element, isNamed: boolean): void {\n\t\tconst labels = isNamed ? this.namingLabels() : [];\n\n\t\tfor (const name of namingAttributes) {\n\t\t\t// `ariaLabelledByElements` holds the attribute, empty\n\t\t\tif (name === 'aria-labelledby' && labels.length > 0) continue;\n\n\t\t\tconst value = this.getAttribute(name);\n\n\t\t\twriteAttribute(target, name, isNamed && value !== null ? value : undefined);\n\t\t}\n\t\tif (labels.length > 0) writeLabelledBy(target, labels);\n\t}\n\n\t// happy-dom matches `:disabled` on no custom element, and an element not form-associated never matches\n\tprotected isDisabled(): boolean {\n\t\treturn this.disabled || this.matches(':disabled');\n\t}\n\n\t// A framework keeps editing the children it owns, so the control copies them rather than taking them\n\tprotected mirrorChildren(\n\t\t{ control, copy, isCopied = () => true }: Mirror,\n\t\tsignal: AbortSignal,\n\t): void {\n\t\tconst slot = this.#slot ?? attachSlot(this);\n\n\t\tthis.#slot = slot;\n\t\tconst mirror = (): void => {\n\t\t\tconst children = [...this.childNodes].filter((child) => child !== control);\n\n\t\t\tcopy(children.filter((child) => isCopied(child)));\n\t\t\tthis.appendOnce(control);\n\t\t\tslot.assign(\n\t\t\t\t...children.filter(\n\t\t\t\t\t(child): child is Element | Text =>\n\t\t\t\t\t\t(child instanceof Element || child instanceof Text) && !isCopied(child),\n\t\t\t\t),\n\t\t\t\tcontrol,\n\t\t\t);\n\t\t};\n\t\tconst observer = new MutationObserver((records) => {\n\t\t\tif (records.some((record) => this.#isCopiedChange(record, control, isCopied))) mirror();\n\t\t});\n\n\t\tmirror();\n\t\tobserver.observe(this, {\n\t\t\tattributes: true,\n\t\t\tcharacterData: true,\n\t\t\tchildList: true,\n\t\t\tsubtree: true,\n\t\t});\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected namingLabels(): Array<Element> {\n\t\treturn [];\n\t}\n\n\tprotected numberAttribute(name: string, fallback: number): number {\n\t\tconst attribute = this.getAttribute(name);\n\t\tif (attribute === null) return fallback;\n\n\t\tconst parsed = Number(attribute);\n\n\t\treturn Number.isFinite(parsed) ? parsed : fallback;\n\t}\n\n\tprotected optionalNumberAttribute(name: string): number | undefined {\n\t\tconst value = this.numberAttribute(name, NaN);\n\n\t\treturn Number.isNaN(value) ? undefined : value;\n\t}\n\n\t// `undefined`, `null` and `false` remove the attribute, as the Lit kits reflect\n\tprotected reflect(name: string, value: boolean | null | number | string | undefined): void {\n\t\tif (value === true) {\n\t\t\tthis.setAttribute(name, '');\n\t\t\treturn;\n\t\t}\n\n\t\twriteAttribute(this, name, value === false || value === null ? undefined : value?.toString());\n\t}\n\n\tprotected toggleState(state: string, isOn: boolean): void {\n\t\tif (isOn) this.internals?.states.add(state);\n\t\telse this.internals?.states.delete(state);\n\t}\n\n\t// A property set before upgrade shadows its accessor\n\tprotected upgradeProperties(...names: Array<keyof this>): void {\n\t\tfor (const name of names) {\n\t\t\tif (!Object.hasOwn(this, name)) continue;\n\n\t\t\tconst value = this[name];\n\n\t\t\tReflect.deleteProperty(this, name);\n\t\t\tthis[name] = value;\n\t\t}\n\t}\n\n\t#childHolding(node: Node): Node | undefined {\n\t\tlet current: Node | null = node;\n\n\t\twhile (current && current.parentNode !== this) current = current.parentNode;\n\n\t\treturn current ?? undefined;\n\t}\n\n\t#focused(): HTMLElement | undefined {\n\t\tconst root = this.getRootNode();\n\t\tconst active =\n\t\t\troot instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;\n\n\t\treturn active instanceof HTMLElement && this.contains(active) ? active : undefined;\n\t}\n\n\t#isCopiedChange(\n\t\trecord: MutationRecord,\n\t\tcontrol: Element,\n\t\tisCopied: (child: Node) => boolean,\n\t): boolean {\n\t\tif (record.target === this) {\n\t\t\tif (record.type === 'attributes') return false;\n\n\t\t\t// The control's own arrival, or every mirror would run twice\n\t\t\treturn (\n\t\t\t\trecord.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control)\n\t\t\t);\n\t\t}\n\n\t\t// A node removed since keeps reporting until delivery, and belongs to no child\n\t\tconst child = this.#childHolding(record.target);\n\n\t\tif (child === undefined || child === control) return false;\n\n\t\t// A child's own attribute can move it out of the copies, which `isCopied` no longer sees\n\t\treturn isCopied(child) || (record.type === 'attributes' && record.target === child);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { SonicDial } from "./elements/dial.js";
|
|
2
2
|
import { SonicKey } from "./elements/key.js";
|
|
3
3
|
import { SonicMeter } from "./elements/meter.js";
|
|
4
|
+
import { SonicNumber } from "./elements/number.js";
|
|
4
5
|
import { SonicSegmented } from "./elements/segmented.js";
|
|
5
6
|
import { SonicSlider } from "./elements/slider.js";
|
|
6
|
-
|
|
7
|
+
import { formatPercent, parsePercent } from "./lib/percent.js";
|
|
8
|
+
export { SonicDial, SonicKey, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, formatPercent, parsePercent };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SonicDial } from "./elements/dial.js";
|
|
2
|
+
import { SonicKey } from "./elements/key.js";
|
|
3
|
+
import { SonicMeter } from "./elements/meter.js";
|
|
4
|
+
import { SonicNumber } from "./elements/number.js";
|
|
5
|
+
import { SonicSegmented } from "./elements/segmented.js";
|
|
6
|
+
import { SonicSlider } from "./elements/slider.js";
|
|
7
|
+
import { formatPercent, parsePercent } from "./lib/percent.js";
|
|
8
|
+
|
|
9
|
+
export { SonicDial, SonicKey, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, formatPercent, parsePercent };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/lib/copy-node.ts
|
|
2
|
+
const copyPrefix = "sonic-copy-";
|
|
3
|
+
const idListAttributes = /* @__PURE__ */ new Set(["aria-describedby", "aria-labelledby"]);
|
|
4
|
+
const hrefAttributes = /* @__PURE__ */ new Set(["href", "xlink:href"]);
|
|
5
|
+
const urlReference = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/g;
|
|
6
|
+
function rewriteReferences(name, value, ids) {
|
|
7
|
+
if (hrefAttributes.has(name)) return value.startsWith("#") && ids.has(value.slice(1)) ? `#${copyPrefix}${value.slice(1)}` : value;
|
|
8
|
+
if (idListAttributes.has(name)) return value.split(/\s+/).map((id) => ids.has(id) ? copyPrefix + id : id).join(" ");
|
|
9
|
+
return value.replaceAll(urlReference, (reference, quote, id) => ids.has(id) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference);
|
|
10
|
+
}
|
|
11
|
+
function prefixIds(element, ids) {
|
|
12
|
+
if (element.id !== "") element.id = copyPrefix + element.id;
|
|
13
|
+
for (const attribute of element.attributes) {
|
|
14
|
+
if (attribute.name === "id") continue;
|
|
15
|
+
const value = rewriteReferences(attribute.name, attribute.value, ids);
|
|
16
|
+
if (value !== attribute.value) attribute.value = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function copyNode(node) {
|
|
20
|
+
const copy = node.cloneNode(true);
|
|
21
|
+
if (!(copy instanceof Element)) return copy;
|
|
22
|
+
const elements = [copy, ...copy.querySelectorAll("*")];
|
|
23
|
+
const ids = new Set(elements.map((element) => element.id).filter((id) => id !== ""));
|
|
24
|
+
if (ids.size === 0) return copy;
|
|
25
|
+
for (const element of elements) prefixIds(element, ids);
|
|
26
|
+
return copy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { copyNode };
|
|
31
|
+
//# sourceMappingURL=copy-node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy-node.js","names":[],"sources":["../../../src/lib/copy-node.ts"],"sourcesContent":["const copyPrefix = 'sonic-copy-';\n\nconst idListAttributes = new Set(['aria-describedby', 'aria-labelledby']);\n\nconst hrefAttributes = new Set(['href', 'xlink:href']);\n\nconst urlReference = /url\\(\\s*(['\"]?)#([^'\")\\s]+)\\1\\s*\\)/g;\n\nfunction rewriteReferences(name: string, value: string, ids: Set<string>): string {\n\tif (hrefAttributes.has(name)) {\n\t\treturn value.startsWith('#') && ids.has(value.slice(1))\n\t\t\t? `#${copyPrefix}${value.slice(1)}`\n\t\t\t: value;\n\t}\n\tif (idListAttributes.has(name)) {\n\t\treturn value\n\t\t\t.split(/\\s+/)\n\t\t\t.map((id) => (ids.has(id) ? copyPrefix + id : id))\n\t\t\t.join(' ');\n\t}\n\n\treturn value.replaceAll(urlReference, (reference, quote: string, id: string) =>\n\t\tids.has(id) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference,\n\t);\n}\n\nfunction prefixIds(element: Element, ids: Set<string>): void {\n\tif (element.id !== '') element.id = copyPrefix + element.id;\n\tfor (const attribute of element.attributes) {\n\t\tif (attribute.name === 'id') continue;\n\n\t\tconst value = rewriteReferences(attribute.name, attribute.value, ids);\n\t\tif (value !== attribute.value) attribute.value = value;\n\t}\n}\n\n// The original stays in the document unrendered, and `url(#id)` in a copy would resolve to its `<defs>`\nexport function copyNode(node: Node): Node {\n\tconst copy = node.cloneNode(true);\n\tif (!(copy instanceof Element)) return copy;\n\n\tconst elements = [copy, ...copy.querySelectorAll('*')];\n\tconst ids = new Set(elements.map((element) => element.id).filter((id) => id !== ''));\n\tif (ids.size === 0) return copy;\n\n\tfor (const element of elements) prefixIds(element, ids);\n\n\treturn copy;\n}\n"],"mappings":";AAAA,MAAM,aAAa;AAEnB,MAAM,mCAAmB,IAAI,IAAI,CAAC,oBAAoB,iBAAiB,CAAC;AAExE,MAAM,iCAAiB,IAAI,IAAI,CAAC,QAAQ,YAAY,CAAC;AAErD,MAAM,eAAe;AAErB,SAAS,kBAAkB,MAAc,OAAe,KAA0B;CACjF,IAAI,eAAe,IAAI,IAAI,GAC1B,OAAO,MAAM,WAAW,GAAG,KAAK,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC,IACnD,IAAI,aAAa,MAAM,MAAM,CAAC,MAC9B;CAEJ,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MACL,MAAM,KAAK,CAAC,CACZ,KAAK,OAAQ,IAAI,IAAI,EAAE,IAAI,aAAa,KAAK,EAAG,CAAC,CACjD,KAAK,GAAG;CAGX,OAAO,MAAM,WAAW,eAAe,WAAW,OAAe,OAChE,IAAI,IAAI,EAAE,IAAI,OAAO,MAAM,GAAG,aAAa,KAAK,MAAM,KAAK,SAC5D;AACD;AAEA,SAAS,UAAU,SAAkB,KAAwB;CAC5D,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK,aAAa,QAAQ;CACzD,KAAK,MAAM,aAAa,QAAQ,YAAY;EAC3C,IAAI,UAAU,SAAS,MAAM;EAE7B,MAAM,QAAQ,kBAAkB,UAAU,MAAM,UAAU,OAAO,GAAG;EACpE,IAAI,UAAU,UAAU,OAAO,UAAU,QAAQ;CAClD;AACD;AAGA,SAAgB,SAAS,MAAkB;CAC1C,MAAM,OAAO,KAAK,UAAU,IAAI;CAChC,IAAI,EAAE,gBAAgB,UAAU,OAAO;CAEvC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,iBAAiB,GAAG,CAAC;CACrD,MAAM,MAAM,IAAI,IAAI,SAAS,KAAK,YAAY,QAAQ,EAAE,CAAC,CAAC,QAAQ,OAAO,OAAO,EAAE,CAAC;CACnF,IAAI,IAAI,SAAS,GAAG,OAAO;CAE3B,KAAK,MAAM,WAAW,UAAU,UAAU,SAAS,GAAG;CAEtD,OAAO;AACR"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/percent.ts
|
|
2
|
+
function formatPercent(value) {
|
|
3
|
+
return `${String(Math.round(value * 100))}%`;
|
|
4
|
+
}
|
|
5
|
+
function parsePercent(text) {
|
|
6
|
+
const digits = text.trim().replace(/\s*%$/, "");
|
|
7
|
+
if (digits === "") return NaN;
|
|
8
|
+
return Number(digits) / 100;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { formatPercent, parsePercent };
|
|
13
|
+
//# sourceMappingURL=percent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"percent.js","names":[],"sources":["../../../src/lib/percent.ts"],"sourcesContent":["export function formatPercent(value: number): string {\n\treturn `${String(Math.round(value * 100))}%`;\n}\n\n// An emptied entry reads as NaN, which the control ignores; `Number('')` is 0\nexport function parsePercent(text: string): number {\n\tconst digits = text.trim().replace(/\\s*%$/, '');\n\tif (digits === '') return NaN;\n\n\treturn Number(digits) / 100;\n}\n"],"mappings":";AAAA,SAAgB,cAAc,OAAuB;CACpD,OAAO,GAAG,OAAO,KAAK,MAAM,QAAQ,GAAG,CAAC,EAAE;AAC3C;AAGA,SAAgB,aAAa,MAAsB;CAClD,MAAM,SAAS,KAAK,KAAK,CAAC,CAAC,QAAQ,SAAS,EAAE;CAC9C,IAAI,WAAW,IAAI,OAAO;CAE1B,OAAO,OAAO,MAAM,IAAI;AACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-px-property.js","names":[],"sources":["../../../src/lib/read-px-property.ts"],"sourcesContent":["// Registered `<length>` properties only; their computed value is always px\nexport function readPxProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(styles.getPropertyValue(property));\n\n\treturn Number.isFinite(parsed) ? parsed : fallback;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CAET,MAAM,SAAS,OAAO,WAAW,OAAO,iBAAiB,QAAQ,CAAC;CAElE,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC3C"}
|