🐍🐍🐍
1
2$css(`
3.number {
4 display: block;
5 line-height: 1.5rem;
6 border-left: 1px solid var(--main-faded);
7 padding-left: 0.5rem;
8}
9
10.number:has(input:focus) {
11 border-left: 3px solid var(--main-solid);
12 padding-left: calc(0.5rem - 2px);
13}
14
15.number label {
16 color: var(--main-solid);
17 min-height: 1em;
18 line-height: 1em;
19 cursor: text;
20}
21
22.number .copyable-value {
23 width: 0;
24 display: inline-block;
25}
26
27.number input[type=number] {
28 background: var(--main-background);
29 color: var(--main-solid);
30 font-family: var(--main-font);
31 transition: border-color 0.2s ease;
32 border-bottom: 1px solid var(--main-faded);
33 min-height: 1rem;
34 line-height: 1rem;
35 width: 5rem;
36}
37
38.number input[type=number] {
39 -webkit-appearance: textfield;
40 -moz-appearance: textfield;
41 appearance: textfield;
42}
43
44.number input[type=number]::-webkit-inner-spin-button,
45.number input[type=number]::-webkit-outer-spin-button {
46 -webkit-appearnce: none;
47}
48
49.number input[type=number]:focus {
50 border-color: var(--main-transparent);
51}
52
53.number input[type=range] {
54 -webkit-appearance: none;
55 appearance: none;
56 width: 100%;
57 outline: none;
58 cursor: pointer;
59 overflow: visible;
60 margin-top: 0.25rem;
61 border: none;
62 border-radius: 2px;
63}
64
65.number input[type=range]:focus {
66 background: none;
67}
68
69/* -webkit: Chromium, Safari, Opera */
70.number input[type=range]::-webkit-slider-runnable-track {
71 background: var(--main-faded);
72 height: 2px;
73}
74
75.number input[type=range]:focus::-webkit-slider-runnable-track {
76 background: var(--main-solid);
77}
78
79/* -moz: Firefox */
80.number input[type=range]::-moz-range-track {
81 background: var(--main-faded);
82 height: 2px;
83}
84
85.number input[type=range]:focus::-moz-range-track {
86 background: var(--main-solid);
87}
88
89.number input[type=range]::-webkit-slider-thumb {
90 -webkit-appearance: none;
91 appearance: none;
92 width: 0.5rem;
93 height: 1rem;
94 border-radius: 2px;
95 background: var(--main-solid);
96 margin-top: calc(1px - 1rem); /* center the thumb on the track */
97
98}
99
100.number input[type=range]::-moz-range-thumb {
101 width: 0.5rem;
102 height: 1rem;
103 border-radius: 2px;
104 border: none; /* cancel default style */
105 background: var(--main-solid);
106}
107
108`);
109
110const defaults = {
111 label: "x",
112 min: -1.0,
113 max: 1.0,
114 limitField: false,
115 step: 0.01,
116 value: 0,
117 onUpdate: null
118};
119
120export async function main(target, spec) {
121 spec = { ...defaults, ...spec };
122
123 const control = document.createElement("div");
124 control.className = "control number";
125
126 // TODO ensure uniqueness more rigorously
127 const name = spec.label.toLowerCase().replace(/\s+/g, "-");
128
129 const label = document.createElement("label");
130 label.innerText = spec.label;
131 label.id = `${name}-label`;
132
133 const label_eq = document.createElement("span");
134 label_eq.innerText = " = ";
135
136 const copyable_value = document.createElement("span");
137 copyable_value.innerText = `${spec.value};\n`;
138 copyable_value.classList = "copyable-value";
139
140 label_eq.setAttribute("aria-hidden", true);
141
142 const slider = document.createElement("input");
143 slider.type = "range";
144 slider.setAttribute("aria-labelledby", label.id);
145 slider.min = spec.min;
146 slider.max = spec.max;
147 slider.step = spec.step;
148 slider.value = spec.value;
149
150 const field = document.createElement("input");
151 field.type = "number";
152 field.setAttribute("aria-labelledby", label.id);
153 if (spec.limitField) {
154 field.min = spec.min;
155 field.max = spec.max;
156 }
157 field.step = spec.step;
158 field.value = spec.value;
159
160 const play_button = $element("button");
161 play_button.innerText = "▶/⏸";
162 // todo alt text
163
164
165 const set = (value) => {
166 slider.value = value;
167 field.value = value;
168 }
169
170 const reset_button = $element("button");
171 reset_button.innerText = "⟳";
172 reset_button.label = "reset";
173 reset_button.addEventListener("click", () => {
174 set(spec.value);
175 spec.onUpdate?.(spec.value, set);
176 });
177
178
179 slider.addEventListener("input", () => {
180 field.value = slider.value;
181 copyable_value.innerText = slider.value;
182 spec.onUpdate?.(slider.value, set);
183 });
184
185 field.addEventListener("input", () => {
186 slider.value = field.value;
187 copyable_value.innerText = field.value;
188 spec.onUpdate?.(field.value, set);
189 });
190
191 target.$with(
192 control.$with(
193 label, label_eq.$with(copyable_value), field,
194 play_button, reset_button,
195 slider
196 )
197 );
198}
199