@xsynaptic/sonic-ui 0.2.0 → 0.4.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 +2 -2
- package/dist/default/define/number.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/dial.js +24 -10
- package/dist/default/elements/key.js +29 -4
- package/dist/default/elements/meter.js +63 -9
- package/dist/default/elements/number.js +46 -0
- package/dist/default/elements/range-element.js +172 -36
- package/dist/default/elements/slider.js +63 -3
- package/dist/default/index.js +2 -1
- package/dist/default/lib/detent.js +17 -0
- package/dist/default/lib/math.js +16 -0
- package/dist/default/lib/number-list.js +14 -0
- package/dist/default/lib/taper.js +17 -4
- 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.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/dial.d.ts +3 -0
- package/dist/dev/elements/dial.js +24 -10
- package/dist/dev/elements/dial.js.map +1 -1
- package/dist/dev/elements/key.d.ts +4 -0
- package/dist/dev/elements/key.js +29 -4
- package/dist/dev/elements/key.js.map +1 -1
- package/dist/dev/elements/meter.d.ts +6 -0
- package/dist/dev/elements/meter.js +63 -9
- package/dist/dev/elements/meter.js.map +1 -1
- 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 +12 -0
- package/dist/dev/elements/range-element.js +172 -36
- package/dist/dev/elements/range-element.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +8 -0
- package/dist/dev/elements/slider.js +63 -3
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/index.d.ts +2 -1
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/detent.js +18 -0
- package/dist/dev/lib/detent.js.map +1 -0
- package/dist/dev/lib/math.js +17 -0
- package/dist/dev/lib/math.js.map +1 -0
- package/dist/dev/lib/number-list.js +15 -0
- package/dist/dev/lib/number-list.js.map +1 -0
- package/dist/dev/lib/read-px-property.js.map +1 -1
- package/dist/dev/lib/taper.js +17 -4
- package/dist/dev/lib/taper.js.map +1 -1
- package/dist/styles/controls.css +2 -0
- package/dist/styles/dial.css +80 -6
- package/dist/styles/key.css +96 -11
- package/dist/styles/led.css +82 -0
- package/dist/styles/material.css +150 -20
- package/dist/styles/meter.css +120 -8
- package/dist/styles/number.css +81 -0
- package/dist/styles/screen.css +6 -39
- package/dist/styles/segmented.css +23 -7
- package/dist/styles/slider.css +143 -30
- package/package.json +4 -2
|
@@ -8,12 +8,15 @@ declare global {
|
|
|
8
8
|
export declare class SonicDial extends SonicRangeElement {
|
|
9
9
|
#private;
|
|
10
10
|
static readonly observedAttributes: string[];
|
|
11
|
+
get endless(): boolean;
|
|
12
|
+
set endless(isEndless: boolean);
|
|
11
13
|
get modulation(): number;
|
|
12
14
|
set modulation(amount: number | undefined);
|
|
13
15
|
connectedCallback(): void;
|
|
14
16
|
protected connect(signal: AbortSignal): void;
|
|
15
17
|
protected focusTarget(): HTMLElement;
|
|
16
18
|
protected render(): void;
|
|
19
|
+
protected wraps(): boolean;
|
|
17
20
|
}
|
|
18
21
|
//#endregion
|
|
19
22
|
//# sourceMappingURL=dial.d.ts.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { template } from "../lib/render.js";
|
|
2
|
-
import { SonicRangeElement } from "./range-element.js";
|
|
1
|
+
import { requireChild, template } from "../lib/render.js";
|
|
3
2
|
import { readPxProperty } from "../lib/read-px-property.js";
|
|
3
|
+
import { SonicRangeElement } from "./range-element.js";
|
|
4
4
|
|
|
5
5
|
//#region src/elements/dial.ts
|
|
6
6
|
const renderDial = template(`
|
|
7
7
|
<div class="sonic-dial" role="slider" tabindex="0">
|
|
8
|
+
<div class="sonic-dial-scale" aria-hidden="true"></div>
|
|
8
9
|
<div class="sonic-dial-notches"></div>
|
|
9
10
|
<div class="sonic-dial-ring"></div>
|
|
10
11
|
<div class="sonic-dial-modulation"></div>
|
|
@@ -25,7 +26,17 @@ const renderDial = template(`
|
|
|
25
26
|
`, HTMLDivElement);
|
|
26
27
|
const fallbackTravelPx = 160;
|
|
27
28
|
var SonicDial = class extends SonicRangeElement {
|
|
28
|
-
static observedAttributes = [
|
|
29
|
+
static observedAttributes = [
|
|
30
|
+
...SonicRangeElement.observedAttributes,
|
|
31
|
+
"endless",
|
|
32
|
+
"modulation"
|
|
33
|
+
];
|
|
34
|
+
get endless() {
|
|
35
|
+
return this.hasAttribute("endless");
|
|
36
|
+
}
|
|
37
|
+
set endless(isEndless) {
|
|
38
|
+
this.reflect("endless", isEndless);
|
|
39
|
+
}
|
|
29
40
|
get modulation() {
|
|
30
41
|
return this.numberAttribute("modulation", 0);
|
|
31
42
|
}
|
|
@@ -34,12 +45,12 @@ var SonicDial = class extends SonicRangeElement {
|
|
|
34
45
|
}
|
|
35
46
|
#dial = renderDial();
|
|
36
47
|
connectedCallback() {
|
|
37
|
-
this.upgradeProperties("modulation");
|
|
48
|
+
this.upgradeProperties("endless", "modulation");
|
|
38
49
|
super.connectedCallback();
|
|
39
50
|
}
|
|
40
51
|
connect(signal) {
|
|
41
52
|
const dial = this.#dial;
|
|
42
|
-
this.
|
|
53
|
+
this.bindScale(dial, requireChild(dial, ".sonic-dial-scale", HTMLDivElement), signal);
|
|
43
54
|
this.render();
|
|
44
55
|
this.checkStyles(dial, "dial.css");
|
|
45
56
|
this.bindGestures(dial, signal, () => ({
|
|
@@ -53,16 +64,19 @@ var SonicDial = class extends SonicRangeElement {
|
|
|
53
64
|
render() {
|
|
54
65
|
const dial = this.#dial;
|
|
55
66
|
const positions = this.positions();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
dial.style.setProperty("--_sonic-dial-value", String(value));
|
|
67
|
+
const [modulationFrom, modulationTo] = this.modulationFractions(this.endless ? 0 : this.modulation);
|
|
68
|
+
dial.style.setProperty("--_sonic-dial-value", String(this.fraction(this.value)));
|
|
59
69
|
dial.style.setProperty("--_sonic-dial-origin", String(this.originFraction()));
|
|
60
|
-
dial.style.setProperty("--_sonic-dial-modulation-from", String(
|
|
61
|
-
dial.style.setProperty("--_sonic-dial-modulation-to", String(
|
|
70
|
+
dial.style.setProperty("--_sonic-dial-modulation-from", String(modulationFrom));
|
|
71
|
+
dial.style.setProperty("--_sonic-dial-modulation-to", String(modulationTo));
|
|
62
72
|
if (positions === void 0) dial.style.removeProperty("--_sonic-dial-positions");
|
|
63
73
|
else dial.style.setProperty("--_sonic-dial-positions", String(positions));
|
|
74
|
+
this.renderScale();
|
|
64
75
|
this.renderAria(dial);
|
|
65
76
|
}
|
|
77
|
+
wraps() {
|
|
78
|
+
return this.endless;
|
|
79
|
+
}
|
|
66
80
|
};
|
|
67
81
|
|
|
68
82
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-pointer\"></div>\n\t\t\t<div class=\"sonic-dial-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-dial-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\nconst fallbackTravelPx = 160;\n\nexport class SonicDial extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'modulation',\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\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tthis.
|
|
1
|
+
{"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-scale\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-pointer\"></div>\n\t\t\t<div class=\"sonic-dial-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-dial-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\nconst fallbackTravelPx = 160;\n\nexport class SonicDial extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'endless',\n\t\t'modulation',\n\t];\n\n\tget endless(): boolean {\n\t\treturn this.hasAttribute('endless');\n\t}\n\n\tset endless(isEndless: boolean) {\n\t\tthis.reflect('endless', isEndless);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute, as on a native element\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\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('endless', 'modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tthis.bindScale(dial, requireChild(dial, '.sonic-dial-scale', HTMLDivElement), signal);\n\t\tthis.render();\n\t\tthis.checkStyles(dial, 'dial.css');\n\t\tthis.bindGestures(dial, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\t// Once per gesture; a computed style read per move forces a style recalc\n\t\t\ttravelPx: readPxProperty(getComputedStyle(dial), '--_sonic-dial-travel', fallbackTravelPx),\n\t\t}));\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#dial;\n\t}\n\n\tprotected render(): void {\n\t\tconst dial = this.#dial;\n\t\tconst positions = this.positions();\n\t\tconst [modulationFrom, modulationTo] = this.modulationFractions(\n\t\t\tthis.endless ? 0 : this.modulation,\n\t\t);\n\n\t\tdial.style.setProperty('--_sonic-dial-value', String(this.fraction(this.value)));\n\t\tdial.style.setProperty('--_sonic-dial-origin', String(this.originFraction()));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-from', String(modulationFrom));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-to', String(modulationTo));\n\t\tif (positions === undefined) dial.style.removeProperty('--_sonic-dial-positions');\n\t\telse dial.style.setProperty('--_sonic-dial-positions', String(positions));\n\t\tthis.renderScale();\n\t\tthis.renderAria(dial);\n\t}\n\n\tprotected override wraps(): boolean {\n\t\treturn this.endless;\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,aAAa,SACP;;;;;;;;;;;;;;;;;;;;IAqBX,cACD;AAEA,MAAM,mBAAmB;AAEzB,IAAa,YAAb,cAA+B,kBAAkB;CAChD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,QAAQ,WAAoB;EAC/B,KAAK,QAAQ,WAAW,SAAS;CAClC;CAGA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAEA,AAAS,QAAQ,WAAW;CAE5B,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,WAAW,YAAY;EAC9C,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,OAAO,KAAK;EAElB,KAAK,UAAU,MAAM,aAAa,MAAM,qBAAqB,cAAc,GAAG,MAAM;EACpF,KAAK,OAAO;EACZ,KAAK,YAAY,MAAM,UAAU;EACjC,KAAK,aAAa,MAAM,eAAe;GACtC,WAAW,UAAU,CAAC,MAAM;GAE5B,UAAU,eAAe,iBAAiB,IAAI,GAAG,wBAAwB,gBAAgB;EAC1F,EAAE;CACH;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,OAAO,KAAK;EAClB,MAAM,YAAY,KAAK,UAAU;EACjC,MAAM,CAAC,gBAAgB,gBAAgB,KAAK,oBAC3C,KAAK,UAAU,IAAI,KAAK,UACzB;EAEA,KAAK,MAAM,YAAY,uBAAuB,OAAO,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;EAC/E,KAAK,MAAM,YAAY,wBAAwB,OAAO,KAAK,eAAe,CAAC,CAAC;EAC5E,KAAK,MAAM,YAAY,iCAAiC,OAAO,cAAc,CAAC;EAC9E,KAAK,MAAM,YAAY,+BAA+B,OAAO,YAAY,CAAC;EAC1E,IAAI,cAAc,QAAW,KAAK,MAAM,eAAe,yBAAyB;OAC3E,KAAK,MAAM,YAAY,2BAA2B,OAAO,SAAS,CAAC;EACxE,KAAK,YAAY;EACjB,KAAK,WAAW,IAAI;CACrB;CAEA,AAAmB,QAAiB;EACnC,OAAO,KAAK;CACb;AACD"}
|
|
@@ -8,12 +8,16 @@ declare global {
|
|
|
8
8
|
export declare class SonicKey extends SonicFormElement {
|
|
9
9
|
#private;
|
|
10
10
|
static readonly observedAttributes: string[];
|
|
11
|
+
get busy(): boolean;
|
|
12
|
+
set busy(isBusy: boolean);
|
|
11
13
|
get defaultPressed(): boolean;
|
|
12
14
|
set defaultPressed(isPressed: boolean);
|
|
13
15
|
get momentary(): boolean;
|
|
14
16
|
set momentary(isMomentary: boolean);
|
|
15
17
|
get pressed(): boolean;
|
|
16
18
|
set pressed(isPressed: boolean | undefined);
|
|
19
|
+
get softDisabled(): boolean;
|
|
20
|
+
set softDisabled(isSoftDisabled: boolean);
|
|
17
21
|
get toggle(): boolean;
|
|
18
22
|
set toggle(isToggle: boolean);
|
|
19
23
|
get value(): string;
|
package/dist/dev/elements/key.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requireChild, template } 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";
|
|
4
5
|
|
|
@@ -10,11 +11,19 @@ const holdKeys = /* @__PURE__ */ new Set([" ", "Enter"]);
|
|
|
10
11
|
var SonicKey = class extends SonicFormElement {
|
|
11
12
|
static observedAttributes = [
|
|
12
13
|
...SonicFormElement.observedAttributes,
|
|
14
|
+
"busy",
|
|
13
15
|
"momentary",
|
|
14
16
|
"pressed",
|
|
17
|
+
"soft-disabled",
|
|
15
18
|
"toggle",
|
|
16
19
|
"value"
|
|
17
20
|
];
|
|
21
|
+
get busy() {
|
|
22
|
+
return this.hasAttribute("busy");
|
|
23
|
+
}
|
|
24
|
+
set busy(isBusy) {
|
|
25
|
+
this.reflect("busy", isBusy);
|
|
26
|
+
}
|
|
18
27
|
get defaultPressed() {
|
|
19
28
|
return this.hasAttribute("pressed");
|
|
20
29
|
}
|
|
@@ -35,6 +44,12 @@ var SonicKey = class extends SonicFormElement {
|
|
|
35
44
|
this.#pressed = isPressed === true;
|
|
36
45
|
this.render();
|
|
37
46
|
}
|
|
47
|
+
get softDisabled() {
|
|
48
|
+
return this.hasAttribute("soft-disabled");
|
|
49
|
+
}
|
|
50
|
+
set softDisabled(isSoftDisabled) {
|
|
51
|
+
this.reflect("soft-disabled", isSoftDisabled);
|
|
52
|
+
}
|
|
38
53
|
get toggle() {
|
|
39
54
|
return this.hasAttribute("toggle");
|
|
40
55
|
}
|
|
@@ -53,7 +68,7 @@ var SonicKey = class extends SonicFormElement {
|
|
|
53
68
|
#pressed = false;
|
|
54
69
|
attributeChangedCallback(name) {
|
|
55
70
|
if (name === "pressed") this.#isDirty = false;
|
|
56
|
-
if (
|
|
71
|
+
if (this.isDisabled() || this.#isSoftDisabled()) this.#release();
|
|
57
72
|
this.render();
|
|
58
73
|
}
|
|
59
74
|
formResetCallback() {
|
|
@@ -65,7 +80,7 @@ var SonicKey = class extends SonicFormElement {
|
|
|
65
80
|
connect(signal) {
|
|
66
81
|
const key = this.#key;
|
|
67
82
|
const cap = requireChild(key, ".sonic-key-cap", HTMLSpanElement);
|
|
68
|
-
this.upgradeProperties("defaultPressed", "momentary", "toggle", "pressed", "value");
|
|
83
|
+
this.upgradeProperties("busy", "defaultPressed", "momentary", "softDisabled", "toggle", "pressed", "value");
|
|
69
84
|
this.mirrorChildren({
|
|
70
85
|
control: key,
|
|
71
86
|
copy: (originals) => {
|
|
@@ -77,7 +92,12 @@ var SonicKey = class extends SonicFormElement {
|
|
|
77
92
|
signal.addEventListener("abort", () => {
|
|
78
93
|
this.#release();
|
|
79
94
|
}, { once: true });
|
|
80
|
-
key.addEventListener("click", () => {
|
|
95
|
+
key.addEventListener("click", (event) => {
|
|
96
|
+
if (this.#isSoftDisabled()) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.stopImmediatePropagation();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
81
101
|
if (!this.toggle) return;
|
|
82
102
|
this.pressed = !this.pressed;
|
|
83
103
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -90,6 +110,8 @@ var SonicKey = class extends SonicFormElement {
|
|
|
90
110
|
render() {
|
|
91
111
|
const key = this.#key;
|
|
92
112
|
key.disabled = this.isDisabled();
|
|
113
|
+
writeAttribute(key, "aria-disabled", this.#isSoftDisabled() ? "true" : void 0);
|
|
114
|
+
writeAttribute(key, "aria-busy", this.busy ? "true" : void 0);
|
|
93
115
|
key.removeAttribute("aria-pressed");
|
|
94
116
|
if (this.toggle) key.setAttribute("aria-pressed", String(this.pressed));
|
|
95
117
|
this.toggleState("pressed", this.pressed);
|
|
@@ -124,7 +146,7 @@ var SonicKey = class extends SonicFormElement {
|
|
|
124
146
|
}, { signal });
|
|
125
147
|
}
|
|
126
148
|
#hold(holder) {
|
|
127
|
-
if (this.isDisabled() || this.#holder !== void 0) return;
|
|
149
|
+
if (this.isDisabled() || this.#isSoftDisabled() || this.#holder !== void 0) return;
|
|
128
150
|
this.#holder = holder;
|
|
129
151
|
this.pressed = true;
|
|
130
152
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -132,6 +154,9 @@ var SonicKey = class extends SonicFormElement {
|
|
|
132
154
|
#isMomentary() {
|
|
133
155
|
return this.momentary && !this.toggle;
|
|
134
156
|
}
|
|
157
|
+
#isSoftDisabled() {
|
|
158
|
+
return this.softDisabled && !this.isDisabled();
|
|
159
|
+
}
|
|
135
160
|
#release() {
|
|
136
161
|
if (this.#holder === void 0) return;
|
|
137
162
|
this.#holder = void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key.js","names":[],"sources":["../../../src/elements/key.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-key': SonicKey;\n\t}\n}\n\nconst renderKey = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-key\" type=\"button\"><span class=\"sonic-key-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicKey extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'momentary',\n\t\t'pressed',\n\t\t'toggle',\n\t\t'value',\n\t];\n\n\t// The `pressed` attribute, as `defaultChecked` holds a checkbox's `checked` attribute\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\t// The live state, which never writes the attribute, as `value` on the range\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget toggle(): boolean {\n\t\treturn this.hasAttribute('toggle');\n\t}\n\n\tset toggle(isToggle: boolean) {\n\t\tthis.reflect('toggle', isToggle);\n\t}\n\n\t// What a pressed toggle submits, reading `on` while unset as a checkbox does\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\t#holder: number | string | undefined;\n\n\t// Re-read from the attribute until a press or a write\n\t// An attribute write clears it, so a framework driving `pressed` keeps control\n\t#isDirty = false;\n\n\treadonly #key = renderKey();\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (name === 'disabled' && this.isDisabled()) this.#release();\n\t\tthis.render();\n\t}\n\n\t// Back to the `pressed` attribute, as a checkbox resets to `checked`\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\t// As a checkbox's label toggles it\n\tprotected override activate(): void {\n\t\tthis.#key.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst key = this.#key;\n\t\tconst cap = requireChild(key, '.sonic-key-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties('defaultPressed', 'momentary', 'toggle', 'pressed', 'value');\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: key,\n\t\t\t\tcopy: (originals) => {\n\t\t\t\t\tcap.replaceChildren(...originals.map((original) => copyNode(original)));\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(key, 'key.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tthis.#release();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\n\t\tkey.addEventListener(\n\t\t\t'click',\n\t\t\t() => {\n\t\t\t\tif (!this.toggle) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(key, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#key;\n\t}\n\n\tprotected render(): void {\n\t\tconst key = this.#key;\n\n\t\tkey.disabled = this.isDisabled();\n\t\tkey.removeAttribute('aria-pressed');\n\t\tif (this.toggle) key.setAttribute('aria-pressed', String(this.pressed));\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(key, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.toggle && this.pressed ? this.value : null;\n\n\t\t// The state restores an unpressed toggle too\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.toggle) this.pressed = state === 'true';\n\t}\n\n\t// Not `click`, which lands a tap late by the finger's dwell\n\t#bindMomentary(key: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst releasePointer = (event: PointerEvent): void => {\n\t\t\tif (event.pointerId === this.#holder) this.#release();\n\t\t};\n\n\t\tkey.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tkey.setPointerCapture(event.pointerId);\n\t\t\t\tthis.#hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener('lostpointercapture', releasePointer, { signal });\n\t\tkey.addEventListener('pointercancel', releasePointer, { signal });\n\t\tkey.addEventListener('pointerup', releasePointer, { signal });\n\t\tkey.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tthis.#hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener(\n\t\t\t'keyup',\n\t\t\t(event) => {\n\t\t\t\tif (event.key === this.#holder) this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// Keyboard holds only; a finger on the next pad takes focus while this one is still held\n\t\tkey.addEventListener(\n\t\t\t'blur',\n\t\t\t() => {\n\t\t\t\tif (typeof this.#holder === 'string') this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#hold(holder: number | string): void {\n\t\tif (this.isDisabled() || this.#holder !== undefined) return;\n\n\t\tthis.#holder = holder;\n\t\tthis.pressed = true;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.toggle;\n\t}\n\n\t#release(): void {\n\t\tif (this.#holder === undefined) return;\n\n\t\tthis.#holder = undefined;\n\t\tthis.pressed = false;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"key.js","names":[],"sources":["../../../src/elements/key.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-key': SonicKey;\n\t}\n}\n\nconst renderKey = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-key\" type=\"button\"><span class=\"sonic-key-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicKey extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'busy',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'toggle',\n\t\t'value',\n\t];\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\t// The `pressed` attribute, as `defaultChecked` holds a checkbox's `checked` attribute\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\t// The live state, which never writes the attribute, as `value` on the range\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget toggle(): boolean {\n\t\treturn this.hasAttribute('toggle');\n\t}\n\n\tset toggle(isToggle: boolean) {\n\t\tthis.reflect('toggle', isToggle);\n\t}\n\n\t// What a pressed toggle submits, reading `on` while unset as a checkbox does\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\t#holder: number | string | undefined;\n\n\t// Re-read from the attribute until a press or a write\n\t// An attribute write clears it, so a framework driving `pressed` keeps control\n\t#isDirty = false;\n\n\treadonly #key = renderKey();\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#release();\n\t\tthis.render();\n\t}\n\n\t// Back to the `pressed` attribute, as a checkbox resets to `checked`\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\t// As a checkbox's label toggles it\n\tprotected override activate(): void {\n\t\tthis.#key.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst key = this.#key;\n\t\tconst cap = requireChild(key, '.sonic-key-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'toggle',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: key,\n\t\t\t\tcopy: (originals) => {\n\t\t\t\t\tcap.replaceChildren(...originals.map((original) => copyNode(original)));\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(key, 'key.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tthis.#release();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\n\t\tkey.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.toggle) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(key, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#key;\n\t}\n\n\tprotected render(): void {\n\t\tconst key = this.#key;\n\n\t\tkey.disabled = this.isDisabled();\n\t\twriteAttribute(key, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(key, 'aria-busy', this.busy ? 'true' : undefined);\n\t\tkey.removeAttribute('aria-pressed');\n\t\tif (this.toggle) key.setAttribute('aria-pressed', String(this.pressed));\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(key, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.toggle && this.pressed ? this.value : null;\n\n\t\t// The state restores an unpressed toggle too\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.toggle) this.pressed = state === 'true';\n\t}\n\n\t// Not `click`, which lands a tap late by the finger's dwell\n\t#bindMomentary(key: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst releasePointer = (event: PointerEvent): void => {\n\t\t\tif (event.pointerId === this.#holder) this.#release();\n\t\t};\n\n\t\tkey.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tkey.setPointerCapture(event.pointerId);\n\t\t\t\tthis.#hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener('lostpointercapture', releasePointer, { signal });\n\t\tkey.addEventListener('pointercancel', releasePointer, { signal });\n\t\tkey.addEventListener('pointerup', releasePointer, { signal });\n\t\tkey.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tthis.#hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener(\n\t\t\t'keyup',\n\t\t\t(event) => {\n\t\t\t\tif (event.key === this.#holder) this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// Keyboard holds only; a finger on the next pad takes focus while this one is still held\n\t\tkey.addEventListener(\n\t\t\t'blur',\n\t\t\t() => {\n\t\t\t\tif (typeof this.#holder === 'string') this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#hold(holder: number | string): void {\n\t\tif (this.isDisabled() || this.#isSoftDisabled() || this.#holder !== undefined) return;\n\n\t\tthis.#holder = holder;\n\t\tthis.pressed = true;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.toggle;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#release(): void {\n\t\tif (this.#holder === undefined) return;\n\n\t\tthis.#holder = undefined;\n\t\tthis.pressed = false;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":";;;;;;AAWA,MAAM,YAAY,SACN;;IAGX,iBACD;AAEA,MAAM,2BAAW,IAAI,IAAI,CAAC,KAAK,OAAO,CAAC;AAEvC,IAAa,WAAb,cAA8B,iBAAiB;CAC9C,OAAyB,qBAAqB;EAC7C,GAAG,iBAAiB;EACpB;EACA;EACA;EACA;EACA;EACA;CACD;CAEA,IAAI,OAAgB;EACnB,OAAO,KAAK,aAAa,MAAM;CAChC;CAEA,IAAI,KAAK,QAAiB;EACzB,KAAK,QAAQ,QAAQ,MAAM;CAC5B;CAGA,IAAI,iBAA0B;EAC7B,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,eAAe,WAAoB;EACtC,KAAK,QAAQ,WAAW,SAAS;CAClC;CAEA,IAAI,YAAqB;EACxB,OAAO,KAAK,aAAa,WAAW;CACrC;CAEA,IAAI,UAAU,aAAsB;EACnC,KAAK,QAAQ,aAAa,WAAW;CACtC;CAGA,IAAI,UAAmB;EACtB,OAAO,KAAK,WAAW,KAAK,WAAW,KAAK;CAC7C;CAEA,IAAI,QAAQ,WAAgC;EAC3C,KAAK,WAAW;EAChB,KAAK,WAAW,cAAc;EAC9B,KAAK,OAAO;CACb;CAEA,IAAI,eAAwB;EAC3B,OAAO,KAAK,aAAa,eAAe;CACzC;CAEA,IAAI,aAAa,gBAAyB;EACzC,KAAK,QAAQ,iBAAiB,cAAc;CAC7C;CAEA,IAAI,SAAkB;EACrB,OAAO,KAAK,aAAa,QAAQ;CAClC;CAEA,IAAI,OAAO,UAAmB;EAC7B,KAAK,QAAQ,UAAU,QAAQ;CAChC;CAGA,IAAI,QAAgB;EACnB,OAAO,KAAK,aAAa,OAAO,KAAK;CACtC;CAEA,IAAI,MAAM,OAA2B;EACpC,KAAK,QAAQ,SAAS,KAAK;CAC5B;CAEA;CAIA,WAAW;CAEX,AAAS,OAAO,UAAU;CAE1B,WAAW;CAEX,yBAAyB,MAAoB;EAC5C,IAAI,SAAS,WAAW,KAAK,WAAW;EACxC,IAAI,KAAK,WAAW,KAAK,KAAK,gBAAgB,GAAG,KAAK,SAAS;EAC/D,KAAK,OAAO;CACb;CAGA,AAAS,oBAA0B;EAClC,KAAK,yBAAyB,SAAS;CACxC;CAGA,AAAmB,WAAiB;EACnC,KAAK,KAAK,MAAM;CACjB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,MAAM,KAAK;EACjB,MAAM,MAAM,aAAa,KAAK,kBAAkB,eAAe;EAE/D,KAAK,kBACJ,QACA,kBACA,aACA,gBACA,UACA,WACA,OACD;EACA,KAAK,eACJ;GACC,SAAS;GACT,OAAO,cAAc;IACpB,IAAI,gBAAgB,GAAG,UAAU,KAAK,aAAa,SAAS,QAAQ,CAAC,CAAC;GACvE;EACD,GACA,MACD;EACA,KAAK,OAAO;EACZ,KAAK,YAAY,KAAK,SAAS;EAC/B,OAAO,iBACN,eACM;GACL,KAAK,SAAS;EACf,GACA,EAAE,MAAM,KAAK,CACd;EAEA,IAAI,iBACH,UACC,UAAU;GAEV,IAAI,KAAK,gBAAgB,GAAG;IAC3B,MAAM,eAAe;IACrB,MAAM,yBAAyB;IAC/B;GACD;GACA,IAAI,CAAC,KAAK,QAAQ;GAElB,KAAK,UAAU,CAAC,KAAK;GACrB,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;EAC1D,GACA,EAAE,OAAO,CACV;EACA,KAAK,eAAe,KAAK,MAAM;CAChC;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,MAAM,KAAK;EAEjB,IAAI,WAAW,KAAK,WAAW;EAC/B,eAAe,KAAK,iBAAiB,KAAK,gBAAgB,IAAI,SAAS,MAAS;EAChF,eAAe,KAAK,aAAa,KAAK,OAAO,SAAS,MAAS;EAC/D,IAAI,gBAAgB,cAAc;EAClC,IAAI,KAAK,QAAQ,IAAI,aAAa,gBAAgB,OAAO,KAAK,OAAO,CAAC;EACtE,KAAK,YAAY,WAAW,KAAK,OAAO;EACxC,KAAK,cAAc,KAAK,IAAI;EAG5B,MAAM,YAAY,KAAK,UAAU,KAAK,UAAU,KAAK,QAAQ;EAG7D,KAAK,eAAe,WAAW,OAAO,KAAK,OAAO,CAAC;CACpD;CAEA,AAAU,aAAa,OAAqB;EAC3C,IAAI,KAAK,QAAQ,KAAK,UAAU,UAAU;CAC3C;CAGA,eAAe,KAAwB,QAA2B;EACjE,MAAM,kBAAkB,UAA8B;GACrD,IAAI,MAAM,cAAc,KAAK,SAAS,KAAK,SAAS;EACrD;EAEA,IAAI,iBACH,gBACC,UAAU;GACV,IAAI,MAAM,WAAW,KAAK,CAAC,KAAK,aAAa,GAAG;GAEhD,IAAI,kBAAkB,MAAM,SAAS;GACrC,KAAK,MAAM,MAAM,SAAS;EAC3B,GACA,EAAE,OAAO,CACV;EACA,IAAI,iBAAiB,sBAAsB,gBAAgB,EAAE,OAAO,CAAC;EACrE,IAAI,iBAAiB,iBAAiB,gBAAgB,EAAE,OAAO,CAAC;EAChE,IAAI,iBAAiB,aAAa,gBAAgB,EAAE,OAAO,CAAC;EAC5D,IAAI,iBACH,YACC,UAAU;GACV,IAAI,MAAM,UAAU,CAAC,SAAS,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,aAAa,GAAG;GAEtE,KAAK,MAAM,MAAM,GAAG;EACrB,GACA,EAAE,OAAO,CACV;EACA,IAAI,iBACH,UACC,UAAU;GACV,IAAI,MAAM,QAAQ,KAAK,SAAS,KAAK,SAAS;EAC/C,GACA,EAAE,OAAO,CACV;EAEA,IAAI,iBACH,cACM;GACL,IAAI,OAAO,KAAK,YAAY,UAAU,KAAK,SAAS;EACrD,GACA,EAAE,OAAO,CACV;CACD;CAEA,MAAM,QAA+B;EACpC,IAAI,KAAK,WAAW,KAAK,KAAK,gBAAgB,KAAK,KAAK,YAAY,QAAW;EAE/E,KAAK,UAAU;EACf,KAAK,UAAU;EACf,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;CAC1D;CAEA,eAAwB;EACvB,OAAO,KAAK,aAAa,CAAC,KAAK;CAChC;CAEA,kBAA2B;EAC1B,OAAO,KAAK,gBAAgB,CAAC,KAAK,WAAW;CAC9C;CAEA,WAAiB;EAChB,IAAI,KAAK,YAAY,QAAW;EAEhC,KAAK,UAAU;EACf,KAAK,UAAU;EACf,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;CAC1D;AACD"}
|
|
@@ -10,12 +10,18 @@ export declare class SonicMeter extends SonicElement {
|
|
|
10
10
|
static readonly observedAttributes: string[];
|
|
11
11
|
get level(): number;
|
|
12
12
|
set level(amplitude: number);
|
|
13
|
+
get lights(): Array<number> | undefined;
|
|
14
|
+
set lights(thresholds: Array<number> | undefined);
|
|
13
15
|
get max(): number;
|
|
14
16
|
set max(decibels: number | undefined);
|
|
15
17
|
get min(): number;
|
|
16
18
|
set min(decibels: number | undefined);
|
|
17
19
|
get orientation(): 'horizontal' | 'vertical';
|
|
18
20
|
set orientation(direction: 'horizontal' | 'vertical' | undefined);
|
|
21
|
+
get origin(): number | undefined;
|
|
22
|
+
set origin(value: number | undefined);
|
|
23
|
+
get scale(): 'db' | 'linear';
|
|
24
|
+
set scale(scale: 'db' | 'linear' | undefined);
|
|
19
25
|
attributeChangedCallback(): void;
|
|
20
26
|
protected connect(signal: AbortSignal): void;
|
|
21
27
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { template } from "../lib/render.js";
|
|
2
2
|
import { SonicElement } from "./sonic-element.js";
|
|
3
|
+
import { parseNumberList } from "../lib/number-list.js";
|
|
4
|
+
import { linearTaper } from "../lib/taper.js";
|
|
3
5
|
|
|
4
6
|
//#region src/elements/meter.ts
|
|
5
7
|
const renderMeter = template(`
|
|
@@ -14,20 +16,30 @@ const holdMs = 1500;
|
|
|
14
16
|
function toDecibels(amplitude) {
|
|
15
17
|
return 20 * Math.log10(Math.abs(amplitude));
|
|
16
18
|
}
|
|
19
|
+
function lightsBelow(lights, zone) {
|
|
20
|
+
return `calc(${lights.map((threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`).join(" + ")})`;
|
|
21
|
+
}
|
|
17
22
|
var SonicMeter = class extends SonicElement {
|
|
18
23
|
static observedAttributes = [
|
|
19
24
|
"disabled",
|
|
25
|
+
"lights",
|
|
20
26
|
"max",
|
|
21
|
-
"min"
|
|
27
|
+
"min",
|
|
28
|
+
"origin",
|
|
29
|
+
"scale"
|
|
22
30
|
];
|
|
23
31
|
get level() {
|
|
24
32
|
return this.#level;
|
|
25
33
|
}
|
|
26
34
|
set level(amplitude) {
|
|
27
35
|
if (!Number.isFinite(amplitude)) return;
|
|
28
|
-
const decibels = toDecibels(amplitude);
|
|
29
36
|
const now = performance.now();
|
|
30
37
|
this.#level = amplitude;
|
|
38
|
+
if (this.scale === "linear") {
|
|
39
|
+
this.#render(now);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const decibels = toDecibels(amplitude);
|
|
31
43
|
this.#target = decibels;
|
|
32
44
|
this.#bar = Math.max(this.#bar, decibels);
|
|
33
45
|
if (decibels >= this.#peak) {
|
|
@@ -38,6 +50,12 @@ var SonicMeter = class extends SonicElement {
|
|
|
38
50
|
this.#render(now);
|
|
39
51
|
this.#schedule(now);
|
|
40
52
|
}
|
|
53
|
+
get lights() {
|
|
54
|
+
return parseNumberList(this.getAttribute("lights"));
|
|
55
|
+
}
|
|
56
|
+
set lights(thresholds) {
|
|
57
|
+
this.reflect("lights", thresholds?.join(" "));
|
|
58
|
+
}
|
|
41
59
|
get max() {
|
|
42
60
|
return this.numberAttribute("max", 0);
|
|
43
61
|
}
|
|
@@ -56,24 +74,39 @@ var SonicMeter = class extends SonicElement {
|
|
|
56
74
|
set orientation(direction) {
|
|
57
75
|
this.reflect("orientation", direction);
|
|
58
76
|
}
|
|
77
|
+
get origin() {
|
|
78
|
+
return this.optionalNumberAttribute("origin");
|
|
79
|
+
}
|
|
80
|
+
set origin(value) {
|
|
81
|
+
this.reflect("origin", value);
|
|
82
|
+
}
|
|
83
|
+
get scale() {
|
|
84
|
+
return this.getAttribute("scale") === "linear" ? "linear" : "db";
|
|
85
|
+
}
|
|
86
|
+
set scale(scale) {
|
|
87
|
+
this.reflect("scale", scale);
|
|
88
|
+
}
|
|
59
89
|
#bar = -Infinity;
|
|
60
90
|
#clipAt = -Infinity;
|
|
61
91
|
#frame;
|
|
62
92
|
#lastFrame;
|
|
63
93
|
#level = 0;
|
|
94
|
+
#lights;
|
|
64
95
|
#meter = renderMeter();
|
|
65
96
|
#peak = -Infinity;
|
|
66
97
|
#peakAt = -Infinity;
|
|
67
98
|
#target = -Infinity;
|
|
68
99
|
attributeChangedCallback() {
|
|
69
100
|
const now = performance.now();
|
|
101
|
+
this.#renderLights();
|
|
70
102
|
this.#render(now);
|
|
71
103
|
this.#schedule(now);
|
|
72
104
|
}
|
|
73
105
|
connect(signal) {
|
|
74
106
|
const now = performance.now();
|
|
75
|
-
this.upgradeProperties("max", "min", "orientation", "level");
|
|
107
|
+
this.upgradeProperties("lights", "max", "min", "orientation", "origin", "scale", "level");
|
|
76
108
|
this.appendOnce(this.#meter);
|
|
109
|
+
this.#renderLights();
|
|
77
110
|
this.#render(now);
|
|
78
111
|
this.#schedule(now);
|
|
79
112
|
this.checkStyles(this.#meter, "meter.css");
|
|
@@ -83,21 +116,25 @@ var SonicMeter = class extends SonicElement {
|
|
|
83
116
|
this.#lastFrame = void 0;
|
|
84
117
|
}, { once: true });
|
|
85
118
|
}
|
|
86
|
-
#fraction(
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
return
|
|
119
|
+
#fraction(level) {
|
|
120
|
+
const lights = this.#lights;
|
|
121
|
+
if (lights) return lights.filter((threshold) => threshold <= level).length / lights.length;
|
|
122
|
+
return linearTaper(this.min, this.max).position(level);
|
|
90
123
|
}
|
|
91
124
|
#isSettled(now) {
|
|
125
|
+
if (this.scale === "linear") return true;
|
|
92
126
|
const floor = this.min;
|
|
93
127
|
return this.#bar <= Math.max(this.#target, floor) && this.#peak <= Math.max(this.#bar, floor) && now - this.#clipAt >= holdMs;
|
|
94
128
|
}
|
|
95
129
|
#render(now) {
|
|
96
130
|
const meter = this.#meter;
|
|
131
|
+
const origin = this.origin === void 0 ? 0 : this.#fraction(this.origin);
|
|
97
132
|
meter.style.setProperty("--_sonic-meter-min", String(this.min));
|
|
98
133
|
meter.style.setProperty("--_sonic-meter-max", String(this.max));
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
meter.style.setProperty("--_sonic-meter-origin", String(origin));
|
|
135
|
+
if (this.disabled || this.scale === "linear") {
|
|
136
|
+
const level = this.disabled ? origin : this.#fraction(this.#level);
|
|
137
|
+
meter.style.setProperty("--_sonic-meter-level", String(level));
|
|
101
138
|
meter.style.setProperty("--_sonic-meter-peak", "0");
|
|
102
139
|
meter.style.setProperty("--_sonic-meter-clipped", "0");
|
|
103
140
|
return;
|
|
@@ -106,6 +143,23 @@ var SonicMeter = class extends SonicElement {
|
|
|
106
143
|
meter.style.setProperty("--_sonic-meter-peak", String(this.#fraction(this.#peak)));
|
|
107
144
|
meter.style.setProperty("--_sonic-meter-clipped", now - this.#clipAt < holdMs ? "1" : "0");
|
|
108
145
|
}
|
|
146
|
+
#renderLights() {
|
|
147
|
+
const meter = this.#meter;
|
|
148
|
+
const lights = this.lights;
|
|
149
|
+
this.#lights = lights;
|
|
150
|
+
this.toggleState("ladder", lights !== void 0);
|
|
151
|
+
if (!lights) {
|
|
152
|
+
for (const name of [
|
|
153
|
+
"count",
|
|
154
|
+
"hot-lights",
|
|
155
|
+
"clip-lights"
|
|
156
|
+
]) meter.style.removeProperty(`--_sonic-meter-${name}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
meter.style.setProperty("--_sonic-meter-count", String(lights.length));
|
|
160
|
+
meter.style.setProperty("--_sonic-meter-hot-lights", lightsBelow(lights, "hot"));
|
|
161
|
+
meter.style.setProperty("--_sonic-meter-clip-lights", lightsBelow(lights, "clip"));
|
|
162
|
+
}
|
|
109
163
|
#schedule(now) {
|
|
110
164
|
if (this.#frame !== void 0 || !this.isConnected || this.#isSettled(now)) return;
|
|
111
165
|
if (this.#lastFrame === void 0) this.#lastFrame = now;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meter.js","names":[],"sources":["../../../src/elements/meter.ts"],"sourcesContent":["import { SonicElement } from '#elements/sonic-element.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-meter': SonicMeter;\n\t}\n}\n\nconst renderMeter = template(\n\t/* HTML */ `\n\t\t<div aria-hidden=\"true\" class=\"sonic-meter\">\n\t\t\t<div class=\"sonic-meter-segments\"></div>\n\t\t\t<div class=\"sonic-meter-level\"></div>\n\t\t\t<div class=\"sonic-meter-clip\"></div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst releaseDecibelsPerSecond = 20;\nconst holdMs = 1500;\n\nfunction toDecibels(amplitude: number): number {\n\treturn 20 * Math.log10(Math.abs(amplitude));\n}\n\n// `level` is a linear amplitude; `min` and `max` are dBFS\nexport class SonicMeter extends SonicElement {\n\tstatic override readonly observedAttributes = ['disabled', 'max', 'min'];\n\n\tget level(): number {\n\t\treturn this.#level;\n\t}\n\n\tset level(amplitude: number) {\n\t\tif (!Number.isFinite(amplitude)) return;\n\n\t\tconst decibels = toDecibels(amplitude);\n\t\tconst now = performance.now();\n\n\t\tthis.#level = amplitude;\n\t\tthis.#target = decibels;\n\t\tthis.#bar = Math.max(this.#bar, decibels);\n\t\tif (decibels >= this.#peak) {\n\t\t\tthis.#peak = decibels;\n\t\t\tthis.#peakAt = now;\n\t\t}\n\t\tif (decibels >= 0) this.#clipAt = now;\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tget max(): number {\n\t\treturn this.numberAttribute('max', 0);\n\t}\n\n\tset max(decibels: number | undefined) {\n\t\tthis.reflect('max', decibels);\n\t}\n\n\tget min(): number {\n\t\treturn this.numberAttribute('min', -60);\n\t}\n\n\tset min(decibels: number | undefined) {\n\t\tthis.reflect('min', decibels);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'horizontal' ? 'horizontal' : 'vertical';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\t#bar = -Infinity;\n\n\t#clipAt = -Infinity;\n\n\t#frame: number | undefined;\n\n\t#lastFrame: number | undefined;\n\n\t#level = 0;\n\n\treadonly #meter = renderMeter();\n\n\t#peak = -Infinity;\n\n\t#peakAt = -Infinity;\n\n\t#target = -Infinity;\n\n\tattributeChangedCallback(): void {\n\t\tconst now = performance.now();\n\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst now = performance.now();\n\n\t\tthis.upgradeProperties('max', 'min', 'orientation', 'level');\n\t\tthis.appendOnce(this.#meter);\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t\tthis.checkStyles(this.#meter, 'meter.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#lastFrame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#fraction(decibels: number): number {\n\t\tconst min = this.min;\n\t\tconst range = this.max - min;\n\n\t\treturn range > 0 ? Math.min(1, Math.max(0, (decibels - min) / range)) : 0;\n\t}\n\n\t#isSettled(now: number): boolean {\n\t\tconst floor = this.min;\n\n\t\treturn (\n\t\t\tthis.#bar <= Math.max(this.#target, floor) &&\n\t\t\tthis.#peak <= Math.max(this.#bar, floor) &&\n\t\t\tnow - this.#clipAt >= holdMs\n\t\t);\n\t}\n\n\t#render(now: number): void {\n\t\tconst meter = this.#meter;\n\n\t\tmeter.style.setProperty('--_sonic-meter-min', String(this.min));\n\t\tmeter.style.setProperty('--_sonic-meter-max', String(this.max));\n\n\t\t// Disabled puts the light out, as on the other controls; the ballistics keep running so it lights up mid-fall\n\t\tif (this.disabled) {\n\t\t\tmeter.style.setProperty('--_sonic-meter-level', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-peak', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-clipped', '0');\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-level', String(this.#fraction(this.#bar)));\n\t\tmeter.style.setProperty('--_sonic-meter-peak', String(this.#fraction(this.#peak)));\n\t\tmeter.style.setProperty('--_sonic-meter-clipped', now - this.#clipAt < holdMs ? '1' : '0');\n\t}\n\n\t#schedule(now: number): void {\n\t\tif (this.#frame !== undefined || !this.isConnected || this.#isSettled(now)) return;\n\n\t\tif (this.#lastFrame === undefined) this.#lastFrame = now;\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t}\n\n\t// Timed by the frame rather than counted, so a 120Hz display falls at the same speed\n\treadonly #tick = (time: number): void => {\n\t\tconst fall = (releaseDecibelsPerSecond * Math.max(0, time - (this.#lastFrame ?? time))) / 1000;\n\n\t\tthis.#frame = undefined;\n\t\tthis.#lastFrame = time;\n\t\tthis.#bar = Math.max(this.#target, this.min, this.#bar - fall);\n\t\tif (time - this.#peakAt >= holdMs) this.#peak = Math.max(this.#bar, this.#peak - fall);\n\t\tthis.#render(time);\n\t\tif (this.#isSettled(time)) {\n\t\t\tthis.#lastFrame = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"meter.js","names":[],"sources":["../../../src/elements/meter.ts"],"sourcesContent":["import { SonicElement } from '#elements/sonic-element.ts';\nimport { parseNumberList } from '#lib/number-list.ts';\nimport { template } from '#lib/render.ts';\nimport { linearTaper } from '#lib/taper.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-meter': SonicMeter;\n\t}\n}\n\nconst renderMeter = template(\n\t/* HTML */ `\n\t\t<div aria-hidden=\"true\" class=\"sonic-meter\">\n\t\t\t<div class=\"sonic-meter-segments\"></div>\n\t\t\t<div class=\"sonic-meter-level\"></div>\n\t\t\t<div class=\"sonic-meter-clip\"></div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst releaseDecibelsPerSecond = 20;\nconst holdMs = 1500;\n\nfunction toDecibels(amplitude: number): number {\n\treturn 20 * Math.log10(Math.abs(amplitude));\n}\n\n// A `calc()` rather than a count, so the zone's token stays live\nfunction lightsBelow(lights: Array<number>, zone: 'clip' | 'hot'): string {\n\tconst terms = lights.map(\n\t\t(threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`,\n\t);\n\n\treturn `calc(${terms.join(' + ')})`;\n}\n\n// On the default `db` scale, `level` is a linear amplitude shown in dBFS; on `linear`, it is in the units of `min` and `max`\nexport class SonicMeter extends SonicElement {\n\tstatic override readonly observedAttributes = [\n\t\t'disabled',\n\t\t'lights',\n\t\t'max',\n\t\t'min',\n\t\t'origin',\n\t\t'scale',\n\t];\n\n\tget level(): number {\n\t\treturn this.#level;\n\t}\n\n\tset level(amplitude: number) {\n\t\tif (!Number.isFinite(amplitude)) return;\n\n\t\tconst now = performance.now();\n\n\t\tthis.#level = amplitude;\n\t\tif (this.scale === 'linear') {\n\t\t\tthis.#render(now);\n\t\t\treturn;\n\t\t}\n\n\t\tconst decibels = toDecibels(amplitude);\n\n\t\tthis.#target = decibels;\n\t\tthis.#bar = Math.max(this.#bar, decibels);\n\t\tif (decibels >= this.#peak) {\n\t\t\tthis.#peak = decibels;\n\t\t\tthis.#peakAt = now;\n\t\t}\n\t\tif (decibels >= 0) this.#clipAt = now;\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tget lights(): Array<number> | undefined {\n\t\treturn parseNumberList(this.getAttribute('lights'));\n\t}\n\n\tset lights(thresholds: Array<number> | undefined) {\n\t\tthis.reflect('lights', thresholds?.join(' '));\n\t}\n\n\tget max(): number {\n\t\treturn this.numberAttribute('max', 0);\n\t}\n\n\tset max(decibels: number | undefined) {\n\t\tthis.reflect('max', decibels);\n\t}\n\n\tget min(): number {\n\t\treturn this.numberAttribute('min', -60);\n\t}\n\n\tset min(decibels: number | undefined) {\n\t\tthis.reflect('min', decibels);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'horizontal' ? 'horizontal' : 'vertical';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute, as on a native element\n\tget origin(): number | undefined {\n\t\treturn this.optionalNumberAttribute('origin');\n\t}\n\n\tset origin(value: number | undefined) {\n\t\tthis.reflect('origin', value);\n\t}\n\n\tget scale(): 'db' | 'linear' {\n\t\treturn this.getAttribute('scale') === 'linear' ? 'linear' : 'db';\n\t}\n\n\tset scale(scale: 'db' | 'linear' | undefined) {\n\t\tthis.reflect('scale', scale);\n\t}\n\n\t#bar = -Infinity;\n\n\t#clipAt = -Infinity;\n\n\t#frame: number | undefined;\n\n\t#lastFrame: number | undefined;\n\n\t#level = 0;\n\n\t// Parsed once per attribute change rather than per frame\n\t#lights: Array<number> | undefined;\n\n\treadonly #meter = renderMeter();\n\n\t#peak = -Infinity;\n\n\t#peakAt = -Infinity;\n\n\t#target = -Infinity;\n\n\tattributeChangedCallback(): void {\n\t\tconst now = performance.now();\n\n\t\tthis.#renderLights();\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst now = performance.now();\n\n\t\tthis.upgradeProperties('lights', 'max', 'min', 'orientation', 'origin', 'scale', 'level');\n\t\tthis.appendOnce(this.#meter);\n\t\tthis.#renderLights();\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t\tthis.checkStyles(this.#meter, 'meter.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#lastFrame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#fraction(level: number): number {\n\t\tconst lights = this.#lights;\n\t\tif (lights) return lights.filter((threshold) => threshold <= level).length / lights.length;\n\n\t\treturn linearTaper(this.min, this.max).position(level);\n\t}\n\n\t#isSettled(now: number): boolean {\n\t\tif (this.scale === 'linear') return true;\n\n\t\tconst floor = this.min;\n\n\t\treturn (\n\t\t\tthis.#bar <= Math.max(this.#target, floor) &&\n\t\t\tthis.#peak <= Math.max(this.#bar, floor) &&\n\t\t\tnow - this.#clipAt >= holdMs\n\t\t);\n\t}\n\n\t#render(now: number): void {\n\t\tconst meter = this.#meter;\n\n\t\tconst origin = this.origin === undefined ? 0 : this.#fraction(this.origin);\n\n\t\tmeter.style.setProperty('--_sonic-meter-min', String(this.min));\n\t\tmeter.style.setProperty('--_sonic-meter-max', String(this.max));\n\t\tmeter.style.setProperty('--_sonic-meter-origin', String(origin));\n\n\t\t// Disabled puts the light out, as on the other controls; the ballistics keep running so it lights up mid-fall\n\t\t// The consumer smooths a linear level, so it has no fall, hold or clip\n\t\tif (this.disabled || this.scale === 'linear') {\n\t\t\tconst level = this.disabled ? origin : this.#fraction(this.#level);\n\n\t\t\tmeter.style.setProperty('--_sonic-meter-level', String(level));\n\t\t\tmeter.style.setProperty('--_sonic-meter-peak', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-clipped', '0');\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-level', String(this.#fraction(this.#bar)));\n\t\tmeter.style.setProperty('--_sonic-meter-peak', String(this.#fraction(this.#peak)));\n\t\tmeter.style.setProperty('--_sonic-meter-clipped', now - this.#clipAt < holdMs ? '1' : '0');\n\t}\n\n\t#renderLights(): void {\n\t\tconst meter = this.#meter;\n\t\tconst lights = this.lights;\n\n\t\tthis.#lights = lights;\n\t\tthis.toggleState('ladder', lights !== undefined);\n\t\tif (!lights) {\n\t\t\tfor (const name of ['count', 'hot-lights', 'clip-lights']) {\n\t\t\t\tmeter.style.removeProperty(`--_sonic-meter-${name}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-count', String(lights.length));\n\t\tmeter.style.setProperty('--_sonic-meter-hot-lights', lightsBelow(lights, 'hot'));\n\t\tmeter.style.setProperty('--_sonic-meter-clip-lights', lightsBelow(lights, 'clip'));\n\t}\n\n\t#schedule(now: number): void {\n\t\tif (this.#frame !== undefined || !this.isConnected || this.#isSettled(now)) return;\n\n\t\tif (this.#lastFrame === undefined) this.#lastFrame = now;\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t}\n\n\t// Timed by the frame rather than counted, so a 120Hz display falls at the same speed\n\treadonly #tick = (time: number): void => {\n\t\tconst fall = (releaseDecibelsPerSecond * Math.max(0, time - (this.#lastFrame ?? time))) / 1000;\n\n\t\tthis.#frame = undefined;\n\t\tthis.#lastFrame = time;\n\t\tthis.#bar = Math.max(this.#target, this.min, this.#bar - fall);\n\t\tif (time - this.#peakAt >= holdMs) this.#peak = Math.max(this.#bar, this.#peak - fall);\n\t\tthis.#render(time);\n\t\tif (this.#isSettled(time)) {\n\t\t\tthis.#lastFrame = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SonicRangeElement } from "./range-element.js";
|
|
2
|
+
//#region src/elements/number.d.ts
|
|
3
|
+
declare global {
|
|
4
|
+
interface HTMLElementTagNameMap {
|
|
5
|
+
'sonic-number': SonicNumber;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare class SonicNumber extends SonicRangeElement {
|
|
9
|
+
#private;
|
|
10
|
+
protected connect(signal: AbortSignal): void;
|
|
11
|
+
protected controlRole(): 'spinbutton';
|
|
12
|
+
protected focusTarget(): HTMLElement;
|
|
13
|
+
protected render(): void;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//# sourceMappingURL=number.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { requireChild, template } from "../lib/render.js";
|
|
2
|
+
import { readPxProperty } from "../lib/read-px-property.js";
|
|
3
|
+
import { SonicRangeElement } from "./range-element.js";
|
|
4
|
+
|
|
5
|
+
//#region src/elements/number.ts
|
|
6
|
+
const renderNumber = template(`
|
|
7
|
+
<div class="sonic-number" role="spinbutton" tabindex="0">
|
|
8
|
+
<span aria-hidden="true" class="sonic-number-value"></span>
|
|
9
|
+
<input
|
|
10
|
+
autocomplete="off"
|
|
11
|
+
class="sonic-number-entry"
|
|
12
|
+
enterkeyhint="done"
|
|
13
|
+
hidden
|
|
14
|
+
spellcheck="false"
|
|
15
|
+
type="text"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
`, HTMLDivElement);
|
|
19
|
+
const fallbackTravelPx = 160;
|
|
20
|
+
var SonicNumber = class extends SonicRangeElement {
|
|
21
|
+
#number = renderNumber();
|
|
22
|
+
#digits = requireChild(this.#number, ".sonic-number-value", HTMLSpanElement);
|
|
23
|
+
connect(signal) {
|
|
24
|
+
const number = this.#number;
|
|
25
|
+
this.appendOnce(number);
|
|
26
|
+
this.render();
|
|
27
|
+
this.checkStyles(number, "number.css");
|
|
28
|
+
this.bindGestures(number, signal, () => ({
|
|
29
|
+
position: (event) => -event.clientY,
|
|
30
|
+
travelPx: readPxProperty(getComputedStyle(number), "--_sonic-number-travel", fallbackTravelPx)
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
controlRole() {
|
|
34
|
+
return "spinbutton";
|
|
35
|
+
}
|
|
36
|
+
focusTarget() {
|
|
37
|
+
return this.#number;
|
|
38
|
+
}
|
|
39
|
+
render() {
|
|
40
|
+
this.#digits.textContent = this.valueText();
|
|
41
|
+
this.renderAria(this.#number);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { SonicNumber };
|
|
47
|
+
//# sourceMappingURL=number.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"number.js","names":[],"sources":["../../../src/elements/number.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-number': SonicNumber;\n\t}\n}\n\nconst renderNumber = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-number\" role=\"spinbutton\" tabindex=\"0\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-number-value\"></span>\n\t\t\t<input\n\t\t\t\tautocomplete=\"off\"\n\t\t\t\tclass=\"sonic-number-entry\"\n\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\thidden\n\t\t\t\tspellcheck=\"false\"\n\t\t\t\ttype=\"text\"\n\t\t\t/>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst fallbackTravelPx = 160;\n\nexport class SonicNumber extends SonicRangeElement {\n\treadonly #number = renderNumber();\n\n\treadonly #digits = requireChild(this.#number, '.sonic-number-value', HTMLSpanElement);\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst number = this.#number;\n\n\t\tthis.appendOnce(number);\n\t\tthis.render();\n\t\tthis.checkStyles(number, 'number.css');\n\t\tthis.bindGestures(number, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: readPxProperty(\n\t\t\t\tgetComputedStyle(number),\n\t\t\t\t'--_sonic-number-travel',\n\t\t\t\tfallbackTravelPx,\n\t\t\t),\n\t\t}));\n\t}\n\n\tprotected override controlRole(): 'spinbutton' {\n\t\treturn 'spinbutton';\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#number;\n\t}\n\n\tprotected render(): void {\n\t\tthis.#digits.textContent = this.valueText();\n\t\tthis.renderAria(this.#number);\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,eAAe,SACT;;;;;;;;;;;;IAaX,cACD;AAEA,MAAM,mBAAmB;AAEzB,IAAa,cAAb,cAAiC,kBAAkB;CAClD,AAAS,UAAU,aAAa;CAEhC,AAAS,UAAU,aAAa,KAAK,SAAS,uBAAuB,eAAe;CAEpF,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EAEpB,KAAK,WAAW,MAAM;EACtB,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,YAAY;EACrC,KAAK,aAAa,QAAQ,eAAe;GACxC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,eACT,iBAAiB,MAAM,GACvB,0BACA,gBACD;EACD,EAAE;CACH;CAEA,AAAmB,cAA4B;EAC9C,OAAO;CACR;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,KAAK,QAAQ,cAAc,KAAK,UAAU;EAC1C,KAAK,WAAW,KAAK,OAAO;CAC7B;AACD"}
|
|
@@ -9,6 +9,8 @@ export declare abstract class SonicRangeElement extends SonicFormElement {
|
|
|
9
9
|
static readonly observedAttributes: string[];
|
|
10
10
|
get default(): number | undefined;
|
|
11
11
|
set default(value: number | undefined);
|
|
12
|
+
get detent(): number | undefined;
|
|
13
|
+
set detent(value: number | undefined);
|
|
12
14
|
get dimmed(): boolean;
|
|
13
15
|
set dimmed(isDimmed: boolean);
|
|
14
16
|
get doublePress(): 'reset' | 'type';
|
|
@@ -35,18 +37,28 @@ export declare abstract class SonicRangeElement extends SonicFormElement {
|
|
|
35
37
|
set taper(curve: 'linear' | 'log' | undefined);
|
|
36
38
|
get value(): number;
|
|
37
39
|
set value(next: number);
|
|
40
|
+
get values(): Array<number> | undefined;
|
|
41
|
+
set values(entries: Array<number> | undefined);
|
|
38
42
|
attributeChangedCallback(name: string): void;
|
|
39
43
|
connectedCallback(): void;
|
|
40
44
|
protected bindGestures(control: HTMLElement, signal: AbortSignal, grab: (event: PointerEvent) => RangeAxis | undefined): void;
|
|
45
|
+
protected bindScale(control: HTMLElement, scale: HTMLElement, signal: AbortSignal): void;
|
|
41
46
|
protected clampRange(next: number): number;
|
|
47
|
+
protected controlRole(): 'slider' | 'spinbutton';
|
|
42
48
|
protected fraction(value: number): number;
|
|
43
49
|
protected input(next: number): boolean;
|
|
50
|
+
protected modulationFractions(amount: number): [number, number];
|
|
44
51
|
protected originFraction(): number;
|
|
45
52
|
protected positions(): number | undefined;
|
|
46
53
|
protected range(): number;
|
|
47
54
|
protected renderAria(control: HTMLElement, orientation?: 'horizontal' | 'vertical'): void;
|
|
55
|
+
protected renderScale(): void;
|
|
48
56
|
protected restoreState(state: string): void;
|
|
57
|
+
protected restValue(): number;
|
|
58
|
+
protected springTarget(): number | undefined;
|
|
49
59
|
protected valueAt(fraction: number): number;
|
|
60
|
+
protected valueText(): string;
|
|
61
|
+
protected wraps(): boolean;
|
|
50
62
|
}
|
|
51
63
|
//#endregion
|
|
52
64
|
//# sourceMappingURL=range-element.d.ts.map
|