@rogieking/figui3 6.14.0 → 6.15.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 +85 -9
- package/components.css +2 -0
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +16 -16
- package/dist/fig.css +1 -1
- package/dist/fig.js +1 -1
- package/fig-lab.css +430 -59
- package/fig-lab.js +855 -59
- package/fig.js +1 -0
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -12,8 +12,800 @@ function figLabBooleanAttribute(element, name) {
|
|
|
12
12
|
return element.hasAttribute(name) && element.getAttribute(name) !== "false";
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/* Field + Switch wrapper */
|
|
16
|
+
class PropskitSwitch extends HTMLElement {
|
|
17
|
+
#field = null;
|
|
18
|
+
#label = null;
|
|
19
|
+
#switch = null;
|
|
20
|
+
#hasCustomLabel = false;
|
|
21
|
+
#observer = null;
|
|
22
|
+
#managedSwitchAttrs = new Set();
|
|
23
|
+
#boundHandleInput = null;
|
|
24
|
+
#boundHandleChange = null;
|
|
25
|
+
|
|
26
|
+
static get observedAttributes() {
|
|
27
|
+
return ["label", "direction"];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
connectedCallback() {
|
|
31
|
+
if (!this.#field) this.#initialize();
|
|
32
|
+
this.#syncField();
|
|
33
|
+
this.#syncSwitchAttributes();
|
|
34
|
+
this.#bindSwitchEvents();
|
|
35
|
+
|
|
36
|
+
if (!this.#observer) {
|
|
37
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
38
|
+
let syncField = false;
|
|
39
|
+
let syncSwitch = false;
|
|
40
|
+
|
|
41
|
+
for (const mutation of mutations) {
|
|
42
|
+
if (mutation.type !== "attributes") continue;
|
|
43
|
+
if (
|
|
44
|
+
mutation.attributeName === "label" ||
|
|
45
|
+
mutation.attributeName === "direction"
|
|
46
|
+
) {
|
|
47
|
+
syncField = true;
|
|
48
|
+
} else {
|
|
49
|
+
syncSwitch = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (syncField) this.#syncField();
|
|
54
|
+
if (syncSwitch) this.#syncSwitchAttributes();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#observer.observe(this, { attributes: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disconnectedCallback() {
|
|
62
|
+
this.#observer?.disconnect();
|
|
63
|
+
this.#unbindSwitchEvents();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
67
|
+
if (oldValue === newValue || !this.#field) return;
|
|
68
|
+
if (name === "label" || name === "direction") this.#syncField();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#initialize() {
|
|
72
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
73
|
+
(node) =>
|
|
74
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
75
|
+
);
|
|
76
|
+
const customLabel = initialChildren.find(
|
|
77
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
78
|
+
);
|
|
79
|
+
const field = document.createElement("fig-field");
|
|
80
|
+
const label = customLabel || document.createElement("label");
|
|
81
|
+
const switchControl = document.createElement("fig-segmented-control");
|
|
82
|
+
const offSegment = document.createElement("fig-segment");
|
|
83
|
+
const onSegment = document.createElement("fig-segment");
|
|
84
|
+
switchControl.setAttribute("sizing", "equal");
|
|
85
|
+
offSegment.setAttribute("value", "off");
|
|
86
|
+
offSegment.textContent = "Off";
|
|
87
|
+
onSegment.setAttribute("value", "on");
|
|
88
|
+
onSegment.textContent = "On";
|
|
89
|
+
switchControl.append(offSegment, onSegment);
|
|
90
|
+
|
|
91
|
+
field.append(label, switchControl);
|
|
92
|
+
this.#field = field;
|
|
93
|
+
this.#label = label;
|
|
94
|
+
this.#switch = switchControl;
|
|
95
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
96
|
+
this.replaceChildren(field);
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#syncField() {
|
|
101
|
+
if (!this.#field || !this.#label) return;
|
|
102
|
+
const hasLabelAttr = this.hasAttribute("label");
|
|
103
|
+
const rawLabel = this.getAttribute("label");
|
|
104
|
+
const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
|
|
105
|
+
|
|
106
|
+
if (isBlankLabel) {
|
|
107
|
+
this.#label.remove();
|
|
108
|
+
} else {
|
|
109
|
+
if (!this.#hasCustomLabel) {
|
|
110
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
111
|
+
}
|
|
112
|
+
if (this.#label.parentElement !== this.#field) {
|
|
113
|
+
this.#field.prepend(this.#label);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.#field.setAttribute(
|
|
118
|
+
"direction",
|
|
119
|
+
this.getAttribute("direction") || "horizontal",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#getForwardedSwitchAttrNames() {
|
|
124
|
+
const reserved = new Set([
|
|
125
|
+
"label",
|
|
126
|
+
"direction",
|
|
127
|
+
"oninput",
|
|
128
|
+
"onchange",
|
|
129
|
+
"class",
|
|
130
|
+
"style",
|
|
131
|
+
"id",
|
|
132
|
+
"size",
|
|
133
|
+
"checked",
|
|
134
|
+
"value",
|
|
135
|
+
]);
|
|
136
|
+
return this.getAttributeNames().filter(
|
|
137
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#syncSwitchAttributes() {
|
|
142
|
+
if (!this.#switch) return;
|
|
143
|
+
const switchAttrs = this.#getForwardedSwitchAttrNames();
|
|
144
|
+
const nextManaged = new Set(switchAttrs);
|
|
145
|
+
|
|
146
|
+
for (const attrName of this.#managedSwitchAttrs) {
|
|
147
|
+
if (!nextManaged.has(attrName)) this.#switch.removeAttribute(attrName);
|
|
148
|
+
}
|
|
149
|
+
for (const attrName of switchAttrs) {
|
|
150
|
+
this.#switch.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.#switch.setAttribute(
|
|
154
|
+
"value",
|
|
155
|
+
figLabBooleanAttribute(this, "checked") ? "on" : "off",
|
|
156
|
+
);
|
|
157
|
+
this.#managedSwitchAttrs = nextManaged;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#bindSwitchEvents() {
|
|
161
|
+
if (!this.#switch) return;
|
|
162
|
+
this.#boundHandleInput ??= this.#forwardSwitchEvent.bind(this, "input");
|
|
163
|
+
this.#boundHandleChange ??= this.#forwardSwitchEvent.bind(this, "change");
|
|
164
|
+
this.#switch.addEventListener("input", this.#boundHandleInput);
|
|
165
|
+
this.#switch.addEventListener("change", this.#boundHandleChange);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#unbindSwitchEvents() {
|
|
169
|
+
if (!this.#switch) return;
|
|
170
|
+
if (this.#boundHandleInput) {
|
|
171
|
+
this.#switch.removeEventListener("input", this.#boundHandleInput);
|
|
172
|
+
}
|
|
173
|
+
if (this.#boundHandleChange) {
|
|
174
|
+
this.#switch.removeEventListener("change", this.#boundHandleChange);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#forwardSwitchEvent(type, event) {
|
|
179
|
+
event.stopImmediatePropagation();
|
|
180
|
+
const checked = this.#switch?.value === "on";
|
|
181
|
+
this.toggleAttribute("checked", checked);
|
|
182
|
+
const detail = {
|
|
183
|
+
checked,
|
|
184
|
+
value: this.getAttribute("value") ?? "",
|
|
185
|
+
};
|
|
186
|
+
this.dispatchEvent(
|
|
187
|
+
new CustomEvent(type, {
|
|
188
|
+
detail,
|
|
189
|
+
bubbles: true,
|
|
190
|
+
cancelable: true,
|
|
191
|
+
composed: true,
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get checked() {
|
|
197
|
+
return this.#switch
|
|
198
|
+
? this.#switch.value === "on"
|
|
199
|
+
: figLabBooleanAttribute(this, "checked");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
set checked(nextChecked) {
|
|
203
|
+
this.toggleAttribute("checked", Boolean(nextChecked));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get value() {
|
|
207
|
+
return this.#switch?.value ?? this.getAttribute("value") ?? "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
set value(nextValue) {
|
|
211
|
+
this.setAttribute("value", nextValue ?? "");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
focus(options) {
|
|
215
|
+
const selected =
|
|
216
|
+
this.#switch?.querySelector("fig-segment[selected]") ||
|
|
217
|
+
this.#switch?.querySelector("fig-segment");
|
|
218
|
+
selected?.focus(options);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
customElements.define("propskit-switch", PropskitSwitch);
|
|
222
|
+
|
|
223
|
+
/* Field + Select wrapper */
|
|
224
|
+
class PropskitSelect extends HTMLElement {
|
|
225
|
+
#field = null;
|
|
226
|
+
#label = null;
|
|
227
|
+
#select = null;
|
|
228
|
+
#hasCustomLabel = false;
|
|
229
|
+
#observer = null;
|
|
230
|
+
#managedSelectAttrs = new Set();
|
|
231
|
+
#boundHandleInput = null;
|
|
232
|
+
#boundHandleChange = null;
|
|
233
|
+
|
|
234
|
+
static get observedAttributes() {
|
|
235
|
+
return ["label", "direction", "aria-label"];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
connectedCallback() {
|
|
239
|
+
if (!this.#field) this.#initialize();
|
|
240
|
+
this.#syncField();
|
|
241
|
+
this.#syncSelectAttributes();
|
|
242
|
+
this.#bindSelectEvents();
|
|
243
|
+
|
|
244
|
+
if (!this.#observer) {
|
|
245
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
246
|
+
let syncField = false;
|
|
247
|
+
let syncSelect = false;
|
|
248
|
+
|
|
249
|
+
for (const mutation of mutations) {
|
|
250
|
+
if (mutation.type !== "attributes") continue;
|
|
251
|
+
if (
|
|
252
|
+
mutation.attributeName === "label" ||
|
|
253
|
+
mutation.attributeName === "direction" ||
|
|
254
|
+
mutation.attributeName === "aria-label"
|
|
255
|
+
) {
|
|
256
|
+
syncField = true;
|
|
257
|
+
} else {
|
|
258
|
+
syncSelect = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (syncField) this.#syncField();
|
|
263
|
+
if (syncSelect) this.#syncSelectAttributes();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.#observer.observe(this, { attributes: true });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
disconnectedCallback() {
|
|
271
|
+
this.#observer?.disconnect();
|
|
272
|
+
this.#unbindSelectEvents();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
276
|
+
if (oldValue === newValue || !this.#field) return;
|
|
277
|
+
if (name === "label" || name === "direction" || name === "aria-label") {
|
|
278
|
+
this.#syncField();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#initialize() {
|
|
283
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
284
|
+
(node) =>
|
|
285
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
286
|
+
);
|
|
287
|
+
const customLabel = initialChildren.find(
|
|
288
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
289
|
+
);
|
|
290
|
+
const field = document.createElement("fig-field");
|
|
291
|
+
const label = customLabel || document.createElement("label");
|
|
292
|
+
const select = document.createElement("fig-dropdown");
|
|
293
|
+
|
|
294
|
+
for (const node of initialChildren) {
|
|
295
|
+
if (node !== customLabel) select.appendChild(node);
|
|
296
|
+
}
|
|
297
|
+
field.append(label, select);
|
|
298
|
+
this.#field = field;
|
|
299
|
+
this.#label = label;
|
|
300
|
+
this.#select = select;
|
|
301
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
302
|
+
this.replaceChildren(field);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#syncField() {
|
|
306
|
+
if (!this.#field || !this.#label || !this.#select) return;
|
|
307
|
+
const hasLabelAttr = this.hasAttribute("label");
|
|
308
|
+
const rawLabel = this.getAttribute("label");
|
|
309
|
+
const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
|
|
310
|
+
|
|
311
|
+
if (isBlankLabel) {
|
|
312
|
+
this.#label.remove();
|
|
313
|
+
} else {
|
|
314
|
+
if (!this.#hasCustomLabel) {
|
|
315
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
316
|
+
}
|
|
317
|
+
if (this.#label.parentElement !== this.#field) {
|
|
318
|
+
this.#field.prepend(this.#label);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.#field.setAttribute(
|
|
323
|
+
"direction",
|
|
324
|
+
this.getAttribute("direction") || "horizontal",
|
|
325
|
+
);
|
|
326
|
+
this.#select.setAttribute(
|
|
327
|
+
"label",
|
|
328
|
+
this.getAttribute("aria-label") ||
|
|
329
|
+
this.#label.textContent?.trim() ||
|
|
330
|
+
"Select",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#getForwardedSelectAttrNames() {
|
|
335
|
+
const reserved = new Set([
|
|
336
|
+
"label",
|
|
337
|
+
"direction",
|
|
338
|
+
"oninput",
|
|
339
|
+
"onchange",
|
|
340
|
+
"class",
|
|
341
|
+
"style",
|
|
342
|
+
"id",
|
|
343
|
+
"size",
|
|
344
|
+
"aria-label",
|
|
345
|
+
]);
|
|
346
|
+
return this.getAttributeNames().filter(
|
|
347
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#syncSelectAttributes() {
|
|
352
|
+
if (!this.#select) return;
|
|
353
|
+
const selectAttrs = this.#getForwardedSelectAttrNames();
|
|
354
|
+
const nextManaged = new Set(selectAttrs);
|
|
355
|
+
|
|
356
|
+
for (const attrName of this.#managedSelectAttrs) {
|
|
357
|
+
if (!nextManaged.has(attrName)) this.#select.removeAttribute(attrName);
|
|
358
|
+
}
|
|
359
|
+
for (const attrName of selectAttrs) {
|
|
360
|
+
this.#select.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.#managedSelectAttrs = nextManaged;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#bindSelectEvents() {
|
|
367
|
+
if (!this.#select) return;
|
|
368
|
+
this.#boundHandleInput ??= this.#forwardSelectEvent.bind(this, "input");
|
|
369
|
+
this.#boundHandleChange ??= this.#forwardSelectEvent.bind(this, "change");
|
|
370
|
+
this.#select.addEventListener("input", this.#boundHandleInput);
|
|
371
|
+
this.#select.addEventListener("change", this.#boundHandleChange);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#unbindSelectEvents() {
|
|
375
|
+
if (!this.#select) return;
|
|
376
|
+
if (this.#boundHandleInput) {
|
|
377
|
+
this.#select.removeEventListener("input", this.#boundHandleInput);
|
|
378
|
+
}
|
|
379
|
+
if (this.#boundHandleChange) {
|
|
380
|
+
this.#select.removeEventListener("change", this.#boundHandleChange);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#forwardSelectEvent(type, event) {
|
|
385
|
+
event.stopImmediatePropagation();
|
|
386
|
+
const value = this.#select?.value ?? "";
|
|
387
|
+
this.setAttribute("value", String(value));
|
|
388
|
+
const detail =
|
|
389
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
390
|
+
? event.detail
|
|
391
|
+
: value;
|
|
392
|
+
this.dispatchEvent(
|
|
393
|
+
new CustomEvent(type, {
|
|
394
|
+
detail,
|
|
395
|
+
bubbles: true,
|
|
396
|
+
cancelable: true,
|
|
397
|
+
composed: true,
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
get value() {
|
|
403
|
+
return this.#select?.value ?? this.getAttribute("value") ?? "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
set value(nextValue) {
|
|
407
|
+
if (nextValue === null || nextValue === undefined) {
|
|
408
|
+
this.removeAttribute("value");
|
|
409
|
+
} else {
|
|
410
|
+
this.setAttribute("value", String(nextValue));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
focus(options) {
|
|
415
|
+
this.#select?.focus(options);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
customElements.define("propskit-select", PropskitSelect);
|
|
419
|
+
|
|
420
|
+
/* Field + Text wrapper */
|
|
421
|
+
class PropskitText extends HTMLElement {
|
|
422
|
+
#field = null;
|
|
423
|
+
#label = null;
|
|
424
|
+
#input = null;
|
|
425
|
+
#hasCustomLabel = false;
|
|
426
|
+
#observer = null;
|
|
427
|
+
#managedInputAttrs = new Set();
|
|
428
|
+
#boundHandleInput = null;
|
|
429
|
+
#boundHandleChange = null;
|
|
430
|
+
|
|
431
|
+
static get observedAttributes() {
|
|
432
|
+
return ["label", "direction", "aria-label"];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
connectedCallback() {
|
|
436
|
+
if (!this.#field) this.#initialize();
|
|
437
|
+
this.#syncField();
|
|
438
|
+
this.#syncInputAttributes();
|
|
439
|
+
this.#bindInputEvents();
|
|
440
|
+
|
|
441
|
+
if (!this.#observer) {
|
|
442
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
443
|
+
let syncField = false;
|
|
444
|
+
let syncInput = false;
|
|
445
|
+
|
|
446
|
+
for (const mutation of mutations) {
|
|
447
|
+
if (mutation.type !== "attributes") continue;
|
|
448
|
+
if (
|
|
449
|
+
mutation.attributeName === "label" ||
|
|
450
|
+
mutation.attributeName === "direction" ||
|
|
451
|
+
mutation.attributeName === "aria-label"
|
|
452
|
+
) {
|
|
453
|
+
syncField = true;
|
|
454
|
+
} else {
|
|
455
|
+
syncInput = true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (syncField) this.#syncField();
|
|
460
|
+
if (syncInput) this.#syncInputAttributes();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.#observer.observe(this, { attributes: true });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
disconnectedCallback() {
|
|
468
|
+
this.#observer?.disconnect();
|
|
469
|
+
this.#unbindInputEvents();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
473
|
+
if (oldValue === newValue || !this.#field) return;
|
|
474
|
+
if (name === "label" || name === "direction" || name === "aria-label") {
|
|
475
|
+
this.#syncField();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#initialize() {
|
|
480
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
481
|
+
(node) =>
|
|
482
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
483
|
+
);
|
|
484
|
+
const customLabel = initialChildren.find(
|
|
485
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
486
|
+
);
|
|
487
|
+
const field = document.createElement("fig-field");
|
|
488
|
+
const label = customLabel || document.createElement("label");
|
|
489
|
+
const input = document.createElement("fig-input-text");
|
|
490
|
+
|
|
491
|
+
for (const node of initialChildren) {
|
|
492
|
+
if (node !== customLabel) input.appendChild(node);
|
|
493
|
+
}
|
|
494
|
+
field.append(label, input);
|
|
495
|
+
this.#field = field;
|
|
496
|
+
this.#label = label;
|
|
497
|
+
this.#input = input;
|
|
498
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
499
|
+
this.replaceChildren(field);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#syncField() {
|
|
503
|
+
if (!this.#field || !this.#label || !this.#input) return;
|
|
504
|
+
const hasLabelAttr = this.hasAttribute("label");
|
|
505
|
+
const rawLabel = this.getAttribute("label");
|
|
506
|
+
const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
|
|
507
|
+
|
|
508
|
+
if (isBlankLabel) {
|
|
509
|
+
this.#label.remove();
|
|
510
|
+
} else {
|
|
511
|
+
if (!this.#hasCustomLabel) {
|
|
512
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
513
|
+
}
|
|
514
|
+
if (this.#label.parentElement !== this.#field) {
|
|
515
|
+
this.#field.prepend(this.#label);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.#field.setAttribute(
|
|
520
|
+
"direction",
|
|
521
|
+
this.getAttribute("direction") || "horizontal",
|
|
522
|
+
);
|
|
523
|
+
this.#input.setAttribute(
|
|
524
|
+
"aria-label",
|
|
525
|
+
this.getAttribute("aria-label") ||
|
|
526
|
+
this.#label.textContent?.trim() ||
|
|
527
|
+
"Text",
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#getForwardedInputAttrNames() {
|
|
532
|
+
const reserved = new Set([
|
|
533
|
+
"label",
|
|
534
|
+
"direction",
|
|
535
|
+
"oninput",
|
|
536
|
+
"onchange",
|
|
537
|
+
"class",
|
|
538
|
+
"style",
|
|
539
|
+
"id",
|
|
540
|
+
"size",
|
|
541
|
+
"aria-label",
|
|
542
|
+
"multiline",
|
|
543
|
+
"resizable",
|
|
544
|
+
]);
|
|
545
|
+
return this.getAttributeNames().filter(
|
|
546
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#syncInputAttributes() {
|
|
551
|
+
if (!this.#input) return;
|
|
552
|
+
const inputAttrs = this.#getForwardedInputAttrNames();
|
|
553
|
+
const nextManaged = new Set(inputAttrs);
|
|
554
|
+
|
|
555
|
+
for (const attrName of this.#managedInputAttrs) {
|
|
556
|
+
if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
|
|
557
|
+
}
|
|
558
|
+
for (const attrName of inputAttrs) {
|
|
559
|
+
this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.#managedInputAttrs = nextManaged;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#bindInputEvents() {
|
|
566
|
+
if (!this.#input) return;
|
|
567
|
+
this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
|
|
568
|
+
this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
|
|
569
|
+
this.#input.addEventListener("input", this.#boundHandleInput);
|
|
570
|
+
this.#input.addEventListener("change", this.#boundHandleChange);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#unbindInputEvents() {
|
|
574
|
+
if (!this.#input) return;
|
|
575
|
+
if (this.#boundHandleInput) {
|
|
576
|
+
this.#input.removeEventListener("input", this.#boundHandleInput);
|
|
577
|
+
}
|
|
578
|
+
if (this.#boundHandleChange) {
|
|
579
|
+
this.#input.removeEventListener("change", this.#boundHandleChange);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#forwardInputEvent(type, event) {
|
|
584
|
+
event.stopImmediatePropagation();
|
|
585
|
+
const value = this.#input?.value ?? "";
|
|
586
|
+
this.setAttribute("value", String(value));
|
|
587
|
+
const detail =
|
|
588
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
589
|
+
? event.detail
|
|
590
|
+
: value;
|
|
591
|
+
this.dispatchEvent(
|
|
592
|
+
new CustomEvent(type, {
|
|
593
|
+
detail,
|
|
594
|
+
bubbles: true,
|
|
595
|
+
cancelable: true,
|
|
596
|
+
composed: true,
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
get value() {
|
|
602
|
+
return this.#input?.value ?? this.getAttribute("value") ?? "";
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
set value(nextValue) {
|
|
606
|
+
if (nextValue === null || nextValue === undefined) {
|
|
607
|
+
this.removeAttribute("value");
|
|
608
|
+
} else {
|
|
609
|
+
this.setAttribute("value", String(nextValue));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
focus(options) {
|
|
614
|
+
this.#input?.focus(options);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
customElements.define("propskit-text", PropskitText);
|
|
618
|
+
|
|
619
|
+
/* Field + Number wrapper */
|
|
620
|
+
class PropskitNumber extends HTMLElement {
|
|
621
|
+
#field = null;
|
|
622
|
+
#label = null;
|
|
623
|
+
#input = null;
|
|
624
|
+
#hasCustomLabel = false;
|
|
625
|
+
#observer = null;
|
|
626
|
+
#managedInputAttrs = new Set();
|
|
627
|
+
#boundHandleInput = null;
|
|
628
|
+
#boundHandleChange = null;
|
|
629
|
+
|
|
630
|
+
static get observedAttributes() {
|
|
631
|
+
return ["label", "direction"];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
connectedCallback() {
|
|
635
|
+
if (!this.#field) this.#initialize();
|
|
636
|
+
this.#syncField();
|
|
637
|
+
this.#syncInputAttributes();
|
|
638
|
+
this.#bindInputEvents();
|
|
639
|
+
|
|
640
|
+
if (!this.#observer) {
|
|
641
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
642
|
+
let syncField = false;
|
|
643
|
+
let syncInput = false;
|
|
644
|
+
|
|
645
|
+
for (const mutation of mutations) {
|
|
646
|
+
if (mutation.type !== "attributes") continue;
|
|
647
|
+
if (
|
|
648
|
+
mutation.attributeName === "label" ||
|
|
649
|
+
mutation.attributeName === "direction"
|
|
650
|
+
) {
|
|
651
|
+
syncField = true;
|
|
652
|
+
} else {
|
|
653
|
+
syncInput = true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (syncField) this.#syncField();
|
|
658
|
+
if (syncInput) this.#syncInputAttributes();
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.#observer.observe(this, { attributes: true });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
disconnectedCallback() {
|
|
666
|
+
this.#observer?.disconnect();
|
|
667
|
+
this.#unbindInputEvents();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
671
|
+
if (oldValue === newValue || !this.#field) return;
|
|
672
|
+
if (name === "label" || name === "direction") this.#syncField();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#initialize() {
|
|
676
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
677
|
+
(node) =>
|
|
678
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
679
|
+
);
|
|
680
|
+
const customLabel = initialChildren.find(
|
|
681
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
682
|
+
);
|
|
683
|
+
const field = document.createElement("fig-field");
|
|
684
|
+
const label = customLabel || document.createElement("label");
|
|
685
|
+
const input = document.createElement("fig-input-number");
|
|
686
|
+
|
|
687
|
+
field.append(label, input);
|
|
688
|
+
this.#field = field;
|
|
689
|
+
this.#label = label;
|
|
690
|
+
this.#input = input;
|
|
691
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
692
|
+
this.replaceChildren(field);
|
|
693
|
+
|
|
694
|
+
for (const node of initialChildren) {
|
|
695
|
+
if (node !== customLabel) input.appendChild(node);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#syncField() {
|
|
700
|
+
if (!this.#field || !this.#label) return;
|
|
701
|
+
const hasLabelAttr = this.hasAttribute("label");
|
|
702
|
+
const rawLabel = this.getAttribute("label");
|
|
703
|
+
const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
|
|
704
|
+
|
|
705
|
+
if (isBlankLabel) {
|
|
706
|
+
this.#label.remove();
|
|
707
|
+
} else {
|
|
708
|
+
if (!this.#hasCustomLabel) {
|
|
709
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
710
|
+
}
|
|
711
|
+
if (this.#label.parentElement !== this.#field) {
|
|
712
|
+
this.#field.prepend(this.#label);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
this.#field.setAttribute(
|
|
717
|
+
"direction",
|
|
718
|
+
this.getAttribute("direction") || "horizontal",
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#getForwardedInputAttrNames() {
|
|
723
|
+
const reserved = new Set([
|
|
724
|
+
"label",
|
|
725
|
+
"direction",
|
|
726
|
+
"oninput",
|
|
727
|
+
"onchange",
|
|
728
|
+
"class",
|
|
729
|
+
"style",
|
|
730
|
+
"id",
|
|
731
|
+
]);
|
|
732
|
+
return this.getAttributeNames().filter(
|
|
733
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#syncInputAttributes() {
|
|
738
|
+
if (!this.#input) return;
|
|
739
|
+
const inputAttrs = this.#getForwardedInputAttrNames();
|
|
740
|
+
const nextManaged = new Set(inputAttrs);
|
|
741
|
+
|
|
742
|
+
for (const attrName of this.#managedInputAttrs) {
|
|
743
|
+
if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
|
|
744
|
+
}
|
|
745
|
+
for (const attrName of inputAttrs) {
|
|
746
|
+
this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
this.#managedInputAttrs = nextManaged;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#bindInputEvents() {
|
|
753
|
+
if (!this.#input) return;
|
|
754
|
+
this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
|
|
755
|
+
this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
|
|
756
|
+
this.#input.addEventListener("input", this.#boundHandleInput);
|
|
757
|
+
this.#input.addEventListener("change", this.#boundHandleChange);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#unbindInputEvents() {
|
|
761
|
+
if (!this.#input) return;
|
|
762
|
+
if (this.#boundHandleInput) {
|
|
763
|
+
this.#input.removeEventListener("input", this.#boundHandleInput);
|
|
764
|
+
}
|
|
765
|
+
if (this.#boundHandleChange) {
|
|
766
|
+
this.#input.removeEventListener("change", this.#boundHandleChange);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#forwardInputEvent(type, event) {
|
|
771
|
+
event.stopImmediatePropagation();
|
|
772
|
+
const detail =
|
|
773
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
774
|
+
? event.detail
|
|
775
|
+
: this.#input?.value;
|
|
776
|
+
if (this.#input?.value !== undefined) {
|
|
777
|
+
this.setAttribute("value", String(this.#input.value));
|
|
778
|
+
}
|
|
779
|
+
this.dispatchEvent(
|
|
780
|
+
new CustomEvent(type, {
|
|
781
|
+
detail,
|
|
782
|
+
bubbles: true,
|
|
783
|
+
cancelable: true,
|
|
784
|
+
composed: true,
|
|
785
|
+
}),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
get value() {
|
|
790
|
+
return this.#input?.value ?? this.getAttribute("value") ?? "";
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
set value(nextValue) {
|
|
794
|
+
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
795
|
+
this.removeAttribute("value");
|
|
796
|
+
} else {
|
|
797
|
+
this.setAttribute("value", String(nextValue));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
focus(options) {
|
|
802
|
+
this.#input?.focus(options);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
customElements.define("propskit-number", PropskitNumber);
|
|
806
|
+
|
|
15
807
|
/* Field + Slider wrapper */
|
|
16
|
-
class
|
|
808
|
+
class PropskitSlider extends HTMLElement {
|
|
17
809
|
#field = null;
|
|
18
810
|
#label = null;
|
|
19
811
|
#slider = null;
|
|
@@ -446,7 +1238,7 @@ class FigFieldSlider extends HTMLElement {
|
|
|
446
1238
|
|
|
447
1239
|
#readElasticDistance() {
|
|
448
1240
|
let raw = getComputedStyle(this)
|
|
449
|
-
.getPropertyValue("--
|
|
1241
|
+
.getPropertyValue("--propskit-slider-elastic-distance")
|
|
450
1242
|
.trim();
|
|
451
1243
|
if (raw.includes("var(") || !raw.endsWith("px")) {
|
|
452
1244
|
const probe = document.createElement("div");
|
|
@@ -454,7 +1246,7 @@ class FigFieldSlider extends HTMLElement {
|
|
|
454
1246
|
position: "absolute",
|
|
455
1247
|
visibility: "hidden",
|
|
456
1248
|
pointerEvents: "none",
|
|
457
|
-
width: "var(--
|
|
1249
|
+
width: "var(--propskit-slider-elastic-distance)",
|
|
458
1250
|
});
|
|
459
1251
|
this.appendChild(probe);
|
|
460
1252
|
raw = getComputedStyle(probe).width;
|
|
@@ -489,10 +1281,10 @@ class FigFieldSlider extends HTMLElement {
|
|
|
489
1281
|
? (this.#elasticHostWidth + stretch) / this.#elasticHostWidth
|
|
490
1282
|
: 1;
|
|
491
1283
|
this.dataset.elasticDragging = "true";
|
|
492
|
-
this.style.setProperty("--
|
|
493
|
-
this.style.setProperty("--
|
|
1284
|
+
this.style.setProperty("--propskit-slider-elastic-size", `${stretch}px`);
|
|
1285
|
+
this.style.setProperty("--propskit-slider-elastic-scale", `${scale}`);
|
|
494
1286
|
this.style.setProperty(
|
|
495
|
-
"--
|
|
1287
|
+
"--propskit-slider-elastic-origin",
|
|
496
1288
|
offset < 0 ? "right center" : "left center",
|
|
497
1289
|
);
|
|
498
1290
|
}
|
|
@@ -507,8 +1299,8 @@ class FigFieldSlider extends HTMLElement {
|
|
|
507
1299
|
|
|
508
1300
|
#clearElasticPull() {
|
|
509
1301
|
this.removeAttribute("data-elastic-dragging");
|
|
510
|
-
this.style.removeProperty("--
|
|
511
|
-
this.style.removeProperty("--
|
|
1302
|
+
this.style.removeProperty("--propskit-slider-elastic-size");
|
|
1303
|
+
this.style.removeProperty("--propskit-slider-elastic-scale");
|
|
512
1304
|
}
|
|
513
1305
|
|
|
514
1306
|
#valueFromPointer(event) {
|
|
@@ -630,7 +1422,7 @@ class FigFieldSlider extends HTMLElement {
|
|
|
630
1422
|
this.#resetToDefault();
|
|
631
1423
|
}
|
|
632
1424
|
}
|
|
633
|
-
customElements.define("
|
|
1425
|
+
customElements.define("propskit-slider", PropskitSlider);
|
|
634
1426
|
|
|
635
1427
|
/* Canvas Control */
|
|
636
1428
|
class FigCanvasControl extends HTMLElement {
|
|
@@ -1828,8 +2620,8 @@ customElements.define("fig-canvas-control", FigCanvasControl);
|
|
|
1828
2620
|
* @attr {string} aspect-ratio - SVG editor aspect ratio.
|
|
1829
2621
|
* @attr {boolean} edit - Whether to show the editor and number fields. Defaults to true.
|
|
1830
2622
|
*/
|
|
1831
|
-
class
|
|
1832
|
-
#waves = [
|
|
2623
|
+
class PropskitOscillator extends HTMLElement {
|
|
2624
|
+
#waves = [PropskitOscillator.#defaultWave()];
|
|
1833
2625
|
#activeWaveIndex = 0;
|
|
1834
2626
|
#precision = 2;
|
|
1835
2627
|
#drawWidth = 240;
|
|
@@ -1935,7 +2727,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
1935
2727
|
|
|
1936
2728
|
get preset() {
|
|
1937
2729
|
const wave = this.#activeWave;
|
|
1938
|
-
return
|
|
2730
|
+
return PropskitOscillator.TYPES.find((type) => type.value === wave.type)?.name;
|
|
1939
2731
|
}
|
|
1940
2732
|
|
|
1941
2733
|
#readInteger(name, fallback) {
|
|
@@ -1991,7 +2783,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
1991
2783
|
|
|
1992
2784
|
this.#waves = nextWaves.map((wave) => this.#normalizeWave(wave));
|
|
1993
2785
|
if (!this.#waves.length) {
|
|
1994
|
-
this.#waves = [
|
|
2786
|
+
this.#waves = [PropskitOscillator.#defaultWave()];
|
|
1995
2787
|
}
|
|
1996
2788
|
this.#activeWaveIndex = Math.min(this.#activeWaveIndex, this.#waves.length - 1);
|
|
1997
2789
|
return true;
|
|
@@ -2021,12 +2813,12 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2021
2813
|
if (!this.#waves[this.#activeWaveIndex]) {
|
|
2022
2814
|
this.#activeWaveIndex = 0;
|
|
2023
2815
|
}
|
|
2024
|
-
return this.#waves[this.#activeWaveIndex] ||
|
|
2816
|
+
return this.#waves[this.#activeWaveIndex] || PropskitOscillator.#defaultWave();
|
|
2025
2817
|
}
|
|
2026
2818
|
|
|
2027
2819
|
#normalizeType(type) {
|
|
2028
2820
|
const normalized = String(type || "").toLowerCase();
|
|
2029
|
-
return
|
|
2821
|
+
return PropskitOscillator.TYPES.some((item) => item.value === normalized)
|
|
2030
2822
|
? normalized
|
|
2031
2823
|
: "sine";
|
|
2032
2824
|
}
|
|
@@ -2051,7 +2843,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2051
2843
|
}
|
|
2052
2844
|
|
|
2053
2845
|
static #labelForType(type) {
|
|
2054
|
-
return
|
|
2846
|
+
return PropskitOscillator.TYPES.find((item) => item.value === type)?.name || "Wave";
|
|
2055
2847
|
}
|
|
2056
2848
|
|
|
2057
2849
|
static waveIcon(type, size = 24) {
|
|
@@ -2061,7 +2853,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2061
2853
|
let d = "";
|
|
2062
2854
|
for (let i = 0; i <= samples; i++) {
|
|
2063
2855
|
const t = i / samples;
|
|
2064
|
-
const value =
|
|
2856
|
+
const value = PropskitOscillator.#waveValue(type, t);
|
|
2065
2857
|
const x = pad + t * draw;
|
|
2066
2858
|
const y = pad + (1 - (value + 1) / 2) * draw;
|
|
2067
2859
|
d += `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`;
|
|
@@ -2099,21 +2891,21 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2099
2891
|
#getInnerHTML() {
|
|
2100
2892
|
const disabled = this.#isDisabled() ? " disabled" : "";
|
|
2101
2893
|
|
|
2102
|
-
return `<div class="
|
|
2103
|
-
<svg viewBox="0 0 ${this.#drawWidth} ${this.#drawHeight}" class="
|
|
2104
|
-
<rect class="
|
|
2105
|
-
<line class="
|
|
2106
|
-
<path class="
|
|
2107
|
-
<circle class="
|
|
2108
|
-
<foreignObject class="
|
|
2109
|
-
<foreignObject class="
|
|
2894
|
+
return `<div class="propskit-oscillator-svg-container">
|
|
2895
|
+
<svg viewBox="0 0 ${this.#drawWidth} ${this.#drawHeight}" class="propskit-oscillator-svg">
|
|
2896
|
+
<rect class="propskit-oscillator-bounds" x="0" y="0" width="${this.#drawWidth}" height="${this.#drawHeight}"></rect>
|
|
2897
|
+
<line class="propskit-oscillator-baseline"></line>
|
|
2898
|
+
<path class="propskit-oscillator-path"></path>
|
|
2899
|
+
<circle class="propskit-oscillator-playhead"></circle>
|
|
2900
|
+
<foreignObject class="propskit-oscillator-handle propskit-oscillator-amplitude-handle" data-handle="amplitude" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Amplitude"><fig-handle size="small" aria-label="Oscillator amplitude handle"${disabled}></fig-handle></fig-tooltip></div></foreignObject>
|
|
2901
|
+
<foreignObject class="propskit-oscillator-handle propskit-oscillator-frequency-handle" data-handle="frequency" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Frequency"><fig-handle size="small" aria-label="Oscillator frequency handle"${disabled}></fig-handle></fig-tooltip></div></foreignObject>
|
|
2110
2902
|
</svg>
|
|
2111
2903
|
</div>
|
|
2112
2904
|
${this.#isEditEnabled() ? this.#getWaveControlsHTML(disabled) : ""}`;
|
|
2113
2905
|
}
|
|
2114
2906
|
|
|
2115
2907
|
#getWaveControlsHTML(disabled) {
|
|
2116
|
-
return `<div class="
|
|
2908
|
+
return `<div class="propskit-oscillator-waves">
|
|
2117
2909
|
${this.#waves.map((wave, index) => this.#getWaveRowHTML(wave, index, disabled)).join("")}
|
|
2118
2910
|
</div>`;
|
|
2119
2911
|
}
|
|
@@ -2121,22 +2913,22 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2121
2913
|
#getWaveRowHTML(wave, index, disabled) {
|
|
2122
2914
|
const removeDisabled = disabled || this.#waves.length <= 1 ? " disabled" : "";
|
|
2123
2915
|
const active = index === this.#activeWaveIndex ? " data-active" : "";
|
|
2124
|
-
const label =
|
|
2916
|
+
const label = PropskitOscillator.#labelForType(wave.type);
|
|
2125
2917
|
const open = this.#expandedWaveIndices.has(index) ? ' open="true"' : ' open="false"';
|
|
2126
|
-
return `<fig-group class="
|
|
2918
|
+
return `<fig-group class="propskit-oscillator-wave" collapsible borderless compact="true"${open} data-wave-index="${index}">
|
|
2127
2919
|
<fig-header borderless>
|
|
2128
2920
|
<h3>${label}</h3>
|
|
2129
2921
|
<fig-tooltip text="Remove form">
|
|
2130
|
-
<fig-button class="
|
|
2922
|
+
<fig-button class="propskit-oscillator-remove-button" variant="ghost" icon data-wave-index="${index}" aria-label="Remove form"${removeDisabled}><fig-icon name="minus"></fig-icon></fig-button>
|
|
2131
2923
|
</fig-tooltip>
|
|
2132
2924
|
<fig-tooltip text="Add form">
|
|
2133
|
-
<fig-button class="
|
|
2925
|
+
<fig-button class="propskit-oscillator-add-type-button" type="select" variant="ghost" icon data-wave-index="${index}" aria-label="Add form"${disabled}>
|
|
2134
2926
|
<fig-icon name="add"></fig-icon>
|
|
2135
|
-
${this.#getWaveTypeDropdownHTML("
|
|
2927
|
+
${this.#getWaveTypeDropdownHTML("propskit-oscillator-add-type", "sine", disabled, index)}
|
|
2136
2928
|
</fig-button>
|
|
2137
2929
|
</fig-tooltip>
|
|
2138
2930
|
</fig-header>
|
|
2139
|
-
<div class="
|
|
2931
|
+
<div class="propskit-oscillator-fields" data-wave-index="${index}"${active}>
|
|
2140
2932
|
${this.#getNumberFieldHTML(index, "frequency", "Frequency", 0.1, 16, 0.1, "")}
|
|
2141
2933
|
${this.#getNumberFieldHTML(index, "amplitude", "Amplitude", -4, 4, 0.1, "")}
|
|
2142
2934
|
${this.#getNumberFieldHTML(index, "phase", "Phase", -360, 360, 1, "°")}
|
|
@@ -2146,44 +2938,44 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2146
2938
|
}
|
|
2147
2939
|
|
|
2148
2940
|
#getWaveTypeDropdownHTML(className, value, disabled, index = null) {
|
|
2149
|
-
const options =
|
|
2941
|
+
const options = PropskitOscillator.TYPES.map((type) => {
|
|
2150
2942
|
const selected = type.value === value ? " selected" : "";
|
|
2151
2943
|
return `<option value="${type.value}"${selected}>
|
|
2152
|
-
${
|
|
2944
|
+
${PropskitOscillator.waveIcon(type.value, 24)}
|
|
2153
2945
|
<label>${type.name}</label>
|
|
2154
2946
|
</option>`;
|
|
2155
2947
|
}).join("");
|
|
2156
2948
|
const indexAttr =
|
|
2157
|
-
index === null ? "" : ` data-wave-index="${
|
|
2949
|
+
index === null ? "" : ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
|
|
2158
2950
|
return `<fig-dropdown class="${className}" value="${value}" experimental="modern" type="dropdown" label="Add form"${indexAttr}${disabled}>${options}</fig-dropdown>`;
|
|
2159
2951
|
}
|
|
2160
2952
|
|
|
2161
2953
|
#getNumberFieldHTML(index, name, label, min, max, step, units) {
|
|
2162
2954
|
const disabled = this.#isDisabled() ? " disabled" : "";
|
|
2163
2955
|
const unitsAttr = units
|
|
2164
|
-
? ` units="${
|
|
2956
|
+
? ` units="${PropskitOscillator.#escapeAttribute(units)}"`
|
|
2165
2957
|
: "";
|
|
2166
|
-
const wave = this.#waves[index] ||
|
|
2167
|
-
return `<
|
|
2958
|
+
const wave = this.#waves[index] || PropskitOscillator.#defaultWave();
|
|
2959
|
+
return `<propskit-slider class="propskit-oscillator-field" label="${label}" direction="horizontal" name="${name}" data-wave-index="${index}" value="${this.#round(wave[name])}" min="${min}" max="${max}" step="${step}" precision="${this.#precision}" elastic="false"${unitsAttr}${disabled}></propskit-slider>`;
|
|
2168
2960
|
}
|
|
2169
2961
|
|
|
2170
2962
|
#cacheRefs() {
|
|
2171
|
-
this.#svg = this.querySelector(".
|
|
2172
|
-
this.#path = this.querySelector(".
|
|
2173
|
-
this.#playhead = this.querySelector(".
|
|
2174
|
-
this.#baseline = this.querySelector(".
|
|
2175
|
-
this.#bounds = this.querySelector(".
|
|
2963
|
+
this.#svg = this.querySelector(".propskit-oscillator-svg");
|
|
2964
|
+
this.#path = this.querySelector(".propskit-oscillator-path");
|
|
2965
|
+
this.#playhead = this.querySelector(".propskit-oscillator-playhead");
|
|
2966
|
+
this.#baseline = this.querySelector(".propskit-oscillator-baseline");
|
|
2967
|
+
this.#bounds = this.querySelector(".propskit-oscillator-bounds");
|
|
2176
2968
|
this.#handleAmplitude = this.querySelector('[data-handle="amplitude"]');
|
|
2177
2969
|
this.#handleFrequency = this.querySelector('[data-handle="frequency"]');
|
|
2178
2970
|
this.#typeControls = Array.from(
|
|
2179
|
-
this.querySelectorAll(".
|
|
2971
|
+
this.querySelectorAll(".propskit-oscillator-wave-type"),
|
|
2180
2972
|
);
|
|
2181
|
-
this.#fields = Array.from(this.querySelectorAll("
|
|
2973
|
+
this.#fields = Array.from(this.querySelectorAll("propskit-slider[name]"));
|
|
2182
2974
|
this.#waveGroups = Array.from(
|
|
2183
|
-
this.querySelectorAll("fig-group.
|
|
2975
|
+
this.querySelectorAll("fig-group.propskit-oscillator-wave"),
|
|
2184
2976
|
);
|
|
2185
2977
|
this.#waveRows = Array.from(
|
|
2186
|
-
this.querySelectorAll(".
|
|
2978
|
+
this.querySelectorAll(".propskit-oscillator-fields"),
|
|
2187
2979
|
);
|
|
2188
2980
|
}
|
|
2189
2981
|
|
|
@@ -2250,7 +3042,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2250
3042
|
#sampleAt(t) {
|
|
2251
3043
|
return this.#waves.reduce((sum, wave) => {
|
|
2252
3044
|
const cycleT = t * wave.frequency;
|
|
2253
|
-
const value =
|
|
3045
|
+
const value = PropskitOscillator.#waveValue(wave.type, cycleT, wave.phase);
|
|
2254
3046
|
return sum + wave.offset + value * wave.amplitude;
|
|
2255
3047
|
}, 0);
|
|
2256
3048
|
}
|
|
@@ -2419,7 +3211,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2419
3211
|
});
|
|
2420
3212
|
}
|
|
2421
3213
|
|
|
2422
|
-
for (const control of this.querySelectorAll(".
|
|
3214
|
+
for (const control of this.querySelectorAll(".propskit-oscillator-add-type")) {
|
|
2423
3215
|
const stopHeaderToggle = (event) => {
|
|
2424
3216
|
event.stopPropagation();
|
|
2425
3217
|
};
|
|
@@ -2431,7 +3223,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2431
3223
|
const insertAfter = this.#indexFromElement(control);
|
|
2432
3224
|
const insertIndex = insertAfter + 1;
|
|
2433
3225
|
const type = this.#normalizeType(event.detail ?? control.value);
|
|
2434
|
-
this.#waves.splice(insertIndex, 0,
|
|
3226
|
+
this.#waves.splice(insertIndex, 0, PropskitOscillator.#defaultWave(type));
|
|
2435
3227
|
this.#reindexExpandedWavesAfterInsert(insertIndex);
|
|
2436
3228
|
this.#activeWaveIndex = insertIndex;
|
|
2437
3229
|
this.#render();
|
|
@@ -2440,7 +3232,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2440
3232
|
});
|
|
2441
3233
|
}
|
|
2442
3234
|
|
|
2443
|
-
for (const button of this.querySelectorAll(".
|
|
3235
|
+
for (const button of this.querySelectorAll(".propskit-oscillator-add-type-button")) {
|
|
2444
3236
|
const stopHeaderToggle = (event) => {
|
|
2445
3237
|
event.stopPropagation();
|
|
2446
3238
|
};
|
|
@@ -2450,7 +3242,7 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2450
3242
|
button.closest("fig-tooltip")?.addEventListener("click", stopHeaderToggle);
|
|
2451
3243
|
}
|
|
2452
3244
|
|
|
2453
|
-
for (const button of this.querySelectorAll(".
|
|
3245
|
+
for (const button of this.querySelectorAll(".propskit-oscillator-remove-button")) {
|
|
2454
3246
|
button.addEventListener("pointerdown", (event) => {
|
|
2455
3247
|
event.stopPropagation();
|
|
2456
3248
|
});
|
|
@@ -2474,10 +3266,10 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2474
3266
|
this.#setupHandle(handle);
|
|
2475
3267
|
}
|
|
2476
3268
|
|
|
2477
|
-
const surface = this.querySelector(".
|
|
3269
|
+
const surface = this.querySelector(".propskit-oscillator-svg-container");
|
|
2478
3270
|
surface?.addEventListener("pointerdown", (event) => {
|
|
2479
3271
|
if (this.#isDisabled()) return;
|
|
2480
|
-
if (event.target?.closest?.(".
|
|
3272
|
+
if (event.target?.closest?.(".propskit-oscillator-handle, fig-handle")) {
|
|
2481
3273
|
return;
|
|
2482
3274
|
}
|
|
2483
3275
|
this.#startDrag(event, "offset");
|
|
@@ -2655,13 +3447,13 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2655
3447
|
return this.#waves.reduce((sum, wave, index) => {
|
|
2656
3448
|
if (index === excludedIndex) return sum;
|
|
2657
3449
|
const cycleT = t * wave.frequency;
|
|
2658
|
-
const value =
|
|
3450
|
+
const value = PropskitOscillator.#waveValue(wave.type, cycleT, wave.phase);
|
|
2659
3451
|
return sum + wave.offset + value * wave.amplitude;
|
|
2660
3452
|
}, 0);
|
|
2661
3453
|
}
|
|
2662
3454
|
|
|
2663
3455
|
#activeWaveValueAt(wave, t) {
|
|
2664
|
-
return
|
|
3456
|
+
return PropskitOscillator.#waveValue(
|
|
2665
3457
|
wave.type,
|
|
2666
3458
|
t * wave.frequency,
|
|
2667
3459
|
wave.phase,
|
|
@@ -2714,13 +3506,13 @@ class FigInputOscillator extends HTMLElement {
|
|
|
2714
3506
|
detail: {
|
|
2715
3507
|
value: this.value,
|
|
2716
3508
|
data: this.data,
|
|
2717
|
-
preset:
|
|
3509
|
+
preset: PropskitOscillator.#labelForType(this.#activeWave.type),
|
|
2718
3510
|
},
|
|
2719
3511
|
}),
|
|
2720
3512
|
);
|
|
2721
3513
|
}
|
|
2722
3514
|
}
|
|
2723
|
-
customElements.define("
|
|
3515
|
+
customElements.define("propskit-oscillator", PropskitOscillator);
|
|
2724
3516
|
|
|
2725
3517
|
/* Angle Input */
|
|
2726
3518
|
/**
|
|
@@ -3259,7 +4051,11 @@ class FigReorder extends HTMLElement {
|
|
|
3259
4051
|
"fig-slider",
|
|
3260
4052
|
"fig-input-number",
|
|
3261
4053
|
"fig-input-text",
|
|
3262
|
-
"
|
|
4054
|
+
"propskit-number",
|
|
4055
|
+
"propskit-select",
|
|
4056
|
+
"propskit-slider",
|
|
4057
|
+
"propskit-switch",
|
|
4058
|
+
"propskit-text",
|
|
3263
4059
|
"fig-checkbox",
|
|
3264
4060
|
"fig-switch",
|
|
3265
4061
|
"fig-combo-input",
|