@rogieking/figui3 6.9.3 → 6.9.5
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 +14 -7
- package/components.css +63 -42
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +7 -7
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +2 -2
- package/dist/fig.css +1 -1
- package/dist/fig.js +28 -28
- package/fig-editor.js +38 -38
- package/fig-lab.css +150 -19
- package/fig-lab.js +321 -5
- package/fig.js +157 -86
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -13,12 +13,34 @@ class FigFieldSlider extends HTMLElement {
|
|
|
13
13
|
#field = null;
|
|
14
14
|
#label = null;
|
|
15
15
|
#slider = null;
|
|
16
|
+
#hasCustomLabel = false;
|
|
16
17
|
#observer = null;
|
|
17
18
|
#managedSliderAttrs = new Set();
|
|
18
19
|
#steppersSyncFrame = 0;
|
|
20
|
+
#focusSyncFrame = 0;
|
|
21
|
+
#rangeInput = null;
|
|
22
|
+
#contextMenu = null;
|
|
23
|
+
#pendingClickTimer = 0;
|
|
24
|
+
#pendingClickValue = null;
|
|
25
|
+
#isElasticTracking = false;
|
|
26
|
+
#elasticMaxPx = 0;
|
|
27
|
+
#elasticRangeRect = null;
|
|
19
28
|
#boundHandleSliderInput = null;
|
|
20
29
|
#boundHandleSliderChange = null;
|
|
21
|
-
#
|
|
30
|
+
#boundHandleElasticPointerDown = this.#handleElasticPointerDown.bind(this);
|
|
31
|
+
#boundHandleElasticPointerMove = this.#handleElasticPointerMove.bind(this);
|
|
32
|
+
#boundHandleElasticPointerEnd = this.#handleElasticPointerEnd.bind(this);
|
|
33
|
+
#boundHandleRangeDoubleClick = this.#handleRangeDoubleClick.bind(this);
|
|
34
|
+
#boundHandleContextMenu = this.#handleContextMenu.bind(this);
|
|
35
|
+
#boundHandleContextMenuChange = this.#handleContextMenuChange.bind(this);
|
|
36
|
+
#ignoredSliderAttrs = new Set([
|
|
37
|
+
"variant",
|
|
38
|
+
"color",
|
|
39
|
+
"text",
|
|
40
|
+
"full",
|
|
41
|
+
"data-elastic-dragging",
|
|
42
|
+
"style",
|
|
43
|
+
]);
|
|
22
44
|
|
|
23
45
|
static get observedAttributes() {
|
|
24
46
|
return ["label", "direction"];
|
|
@@ -32,6 +54,16 @@ class FigFieldSlider extends HTMLElement {
|
|
|
32
54
|
this.#syncField();
|
|
33
55
|
this.#syncSliderAttributes();
|
|
34
56
|
this.#bindSliderEvents();
|
|
57
|
+
this.#queueFocusDelegationSync();
|
|
58
|
+
this.removeEventListener("pointerdown", this.#boundHandleElasticPointerDown, {
|
|
59
|
+
capture: true,
|
|
60
|
+
});
|
|
61
|
+
this.addEventListener("pointerdown", this.#boundHandleElasticPointerDown, {
|
|
62
|
+
capture: true,
|
|
63
|
+
passive: true,
|
|
64
|
+
});
|
|
65
|
+
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
66
|
+
this.addEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
35
67
|
|
|
36
68
|
if (!this.#observer) {
|
|
37
69
|
this.#observer = new MutationObserver((mutations) => {
|
|
@@ -58,7 +90,10 @@ class FigFieldSlider extends HTMLElement {
|
|
|
58
90
|
}
|
|
59
91
|
|
|
60
92
|
if (syncField) this.#syncField();
|
|
61
|
-
if (syncSlider)
|
|
93
|
+
if (syncSlider) {
|
|
94
|
+
this.#syncSliderAttributes();
|
|
95
|
+
this.#queueFocusDelegationSync();
|
|
96
|
+
}
|
|
62
97
|
});
|
|
63
98
|
}
|
|
64
99
|
|
|
@@ -71,7 +106,19 @@ class FigFieldSlider extends HTMLElement {
|
|
|
71
106
|
cancelAnimationFrame(this.#steppersSyncFrame);
|
|
72
107
|
this.#steppersSyncFrame = 0;
|
|
73
108
|
}
|
|
109
|
+
if (this.#focusSyncFrame) {
|
|
110
|
+
cancelAnimationFrame(this.#focusSyncFrame);
|
|
111
|
+
this.#focusSyncFrame = 0;
|
|
112
|
+
}
|
|
113
|
+
this.#clearPendingClick();
|
|
114
|
+
this.#resetElasticPull();
|
|
115
|
+
this.#unbindRangeInput();
|
|
74
116
|
this.#unbindSliderEvents();
|
|
117
|
+
this.removeEventListener("pointerdown", this.#boundHandleElasticPointerDown, {
|
|
118
|
+
capture: true,
|
|
119
|
+
});
|
|
120
|
+
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
121
|
+
this.#contextMenu?.removeEventListener("change", this.#boundHandleContextMenuChange);
|
|
75
122
|
}
|
|
76
123
|
|
|
77
124
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -88,8 +135,11 @@ class FigFieldSlider extends HTMLElement {
|
|
|
88
135
|
);
|
|
89
136
|
});
|
|
90
137
|
|
|
138
|
+
const customLabel = initialChildren.find(
|
|
139
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
140
|
+
);
|
|
91
141
|
const field = document.createElement("fig-field");
|
|
92
|
-
const label = document.createElement("label");
|
|
142
|
+
const label = customLabel || document.createElement("label");
|
|
93
143
|
const slider = document.createElement("fig-slider");
|
|
94
144
|
slider.setAttribute("text", "true");
|
|
95
145
|
for (const attrName of this.#getForwardedSliderAttrNames()) {
|
|
@@ -102,14 +152,32 @@ class FigFieldSlider extends HTMLElement {
|
|
|
102
152
|
this.#field = field;
|
|
103
153
|
this.#label = label;
|
|
104
154
|
this.#slider = slider;
|
|
155
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
105
156
|
|
|
106
157
|
this.replaceChildren(field);
|
|
158
|
+
this.#setupContextMenu();
|
|
107
159
|
|
|
108
160
|
for (const node of initialChildren) {
|
|
161
|
+
if (node === customLabel) continue;
|
|
109
162
|
this.#slider.appendChild(node);
|
|
110
163
|
}
|
|
111
164
|
}
|
|
112
165
|
|
|
166
|
+
#setupContextMenu() {
|
|
167
|
+
const menu = document.createElement("fig-menu");
|
|
168
|
+
menu.setAttribute("position", "bottom left");
|
|
169
|
+
menu.setAttribute("offset", "0 0");
|
|
170
|
+
|
|
171
|
+
const resetItem = document.createElement("fig-menu-item");
|
|
172
|
+
resetItem.setAttribute("value", "reset-default");
|
|
173
|
+
resetItem.textContent = "Reset to default";
|
|
174
|
+
menu.appendChild(resetItem);
|
|
175
|
+
menu.addEventListener("change", this.#boundHandleContextMenuChange);
|
|
176
|
+
|
|
177
|
+
this.#contextMenu = menu;
|
|
178
|
+
this.appendChild(menu);
|
|
179
|
+
}
|
|
180
|
+
|
|
113
181
|
#syncField() {
|
|
114
182
|
if (!this.#field || !this.#label) return;
|
|
115
183
|
const hasLabelAttr = this.hasAttribute("label");
|
|
@@ -121,7 +189,9 @@ class FigFieldSlider extends HTMLElement {
|
|
|
121
189
|
this.#label.remove();
|
|
122
190
|
}
|
|
123
191
|
} else {
|
|
124
|
-
this.#
|
|
192
|
+
if (!this.#hasCustomLabel) {
|
|
193
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
194
|
+
}
|
|
125
195
|
if (this.#label.parentElement !== this.#field) {
|
|
126
196
|
this.#field.prepend(this.#label);
|
|
127
197
|
}
|
|
@@ -174,7 +244,7 @@ class FigFieldSlider extends HTMLElement {
|
|
|
174
244
|
if (sliderType === "opacity") {
|
|
175
245
|
this.#slider.style.setProperty(
|
|
176
246
|
"--color",
|
|
177
|
-
"
|
|
247
|
+
"light-dark(#444444, #e6e6e6)",
|
|
178
248
|
);
|
|
179
249
|
} else {
|
|
180
250
|
this.#slider.style.removeProperty("--color");
|
|
@@ -204,7 +274,242 @@ class FigFieldSlider extends HTMLElement {
|
|
|
204
274
|
this.#steppersSyncFrame = requestAnimationFrame(() => {
|
|
205
275
|
this.#steppersSyncFrame = 0;
|
|
206
276
|
this.#syncSteppersToNumberInput();
|
|
277
|
+
this.#syncFocusDelegation();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#queueFocusDelegationSync() {
|
|
282
|
+
if (this.#focusSyncFrame) {
|
|
283
|
+
cancelAnimationFrame(this.#focusSyncFrame);
|
|
284
|
+
}
|
|
285
|
+
this.#focusSyncFrame = requestAnimationFrame(() => {
|
|
286
|
+
this.#focusSyncFrame = 0;
|
|
287
|
+
this.#syncFocusDelegation();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#syncFocusDelegation() {
|
|
292
|
+
const rangeInput = this.#slider?.querySelector('input[type="range"]');
|
|
293
|
+
const numberInput = this.#slider?.querySelector("fig-input-number input");
|
|
294
|
+
if (rangeInput !== this.#rangeInput) {
|
|
295
|
+
this.#bindRangeInput(rangeInput);
|
|
296
|
+
}
|
|
297
|
+
rangeInput?.removeAttribute("tabindex");
|
|
298
|
+
numberInput?.setAttribute("tabindex", "-1");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#bindRangeInput(rangeInput) {
|
|
302
|
+
this.#unbindRangeInput();
|
|
303
|
+
this.#rangeInput = rangeInput;
|
|
304
|
+
if (!this.#rangeInput) return;
|
|
305
|
+
this.#rangeInput.addEventListener("dblclick", this.#boundHandleRangeDoubleClick, {
|
|
306
|
+
capture: true,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#unbindRangeInput() {
|
|
311
|
+
if (!this.#rangeInput) return;
|
|
312
|
+
this.#rangeInput.removeEventListener("dblclick", this.#boundHandleRangeDoubleClick, {
|
|
313
|
+
capture: true,
|
|
314
|
+
});
|
|
315
|
+
this.#rangeInput = null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#handleRangeDoubleClick(event) {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
event.stopImmediatePropagation();
|
|
321
|
+
this.#resetToDefault();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#handleElasticPointerDown(event) {
|
|
325
|
+
if (event.button !== 0 || this.hasAttribute("disabled")) return;
|
|
326
|
+
if (event.target?.closest?.("fig-input-number")) return;
|
|
327
|
+
const rangeInput =
|
|
328
|
+
this.#slider?.querySelector('input[type="range"]') ?? this.#rangeInput;
|
|
329
|
+
if (!rangeInput) return;
|
|
330
|
+
this.#rangeInput = rangeInput;
|
|
331
|
+
this.#isElasticTracking = true;
|
|
332
|
+
this.#elasticMaxPx = this.#readElasticDistance();
|
|
333
|
+
const rect = rangeInput.getBoundingClientRect();
|
|
334
|
+
this.#elasticRangeRect = {
|
|
335
|
+
left: rect.left,
|
|
336
|
+
right: rect.right,
|
|
337
|
+
width: rect.width,
|
|
338
|
+
};
|
|
339
|
+
window.addEventListener("pointermove", this.#boundHandleElasticPointerMove, {
|
|
340
|
+
passive: true,
|
|
207
341
|
});
|
|
342
|
+
window.addEventListener("pointerup", this.#boundHandleElasticPointerEnd, {
|
|
343
|
+
once: true,
|
|
344
|
+
});
|
|
345
|
+
window.addEventListener("pointercancel", this.#boundHandleElasticPointerEnd, {
|
|
346
|
+
once: true,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#handleElasticPointerMove(event) {
|
|
351
|
+
if (!this.#isElasticTracking) return;
|
|
352
|
+
this.#updateElasticPull(event.clientX);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#handleElasticPointerEnd() {
|
|
356
|
+
window.removeEventListener("pointermove", this.#boundHandleElasticPointerMove);
|
|
357
|
+
window.removeEventListener("pointerup", this.#boundHandleElasticPointerEnd);
|
|
358
|
+
window.removeEventListener("pointercancel", this.#boundHandleElasticPointerEnd);
|
|
359
|
+
this.#isElasticTracking = false;
|
|
360
|
+
this.#resetElasticPull();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#handleContextMenu(event) {
|
|
364
|
+
if (this.hasAttribute("disabled")) return;
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
event.stopImmediatePropagation();
|
|
368
|
+
this.#clearPendingClick();
|
|
369
|
+
this.#showContextMenuAfterPointerRelease(event.clientX, event.clientY);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#showContextMenuAfterPointerRelease(x, y) {
|
|
373
|
+
let opened = false;
|
|
374
|
+
let fallbackTimer = 0;
|
|
375
|
+
const openMenu = () => {
|
|
376
|
+
if (opened) return;
|
|
377
|
+
opened = true;
|
|
378
|
+
window.clearTimeout(fallbackTimer);
|
|
379
|
+
window.removeEventListener("pointerup", openMenu, true);
|
|
380
|
+
window.removeEventListener("pointercancel", openMenu, true);
|
|
381
|
+
requestAnimationFrame(() => {
|
|
382
|
+
this.#contextMenu?.showAt?.(x, y);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
window.addEventListener("pointerup", openMenu, { once: true, capture: true });
|
|
386
|
+
window.addEventListener("pointercancel", openMenu, {
|
|
387
|
+
once: true,
|
|
388
|
+
capture: true,
|
|
389
|
+
});
|
|
390
|
+
fallbackTimer = window.setTimeout(openMenu, 180);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#handleContextMenuChange(event) {
|
|
394
|
+
event.stopPropagation();
|
|
395
|
+
if (event.detail?.value !== "reset-default") return;
|
|
396
|
+
this.#resetToDefault();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#clearPendingClick() {
|
|
400
|
+
if (this.#pendingClickTimer) {
|
|
401
|
+
clearTimeout(this.#pendingClickTimer);
|
|
402
|
+
this.#pendingClickTimer = 0;
|
|
403
|
+
}
|
|
404
|
+
this.#pendingClickValue = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#readElasticDistance() {
|
|
408
|
+
let raw = getComputedStyle(this)
|
|
409
|
+
.getPropertyValue("--fig-field-slider-elastic-distance")
|
|
410
|
+
.trim();
|
|
411
|
+
if (raw.includes("var(") || !raw.endsWith("px")) {
|
|
412
|
+
const probe = document.createElement("div");
|
|
413
|
+
Object.assign(probe.style, {
|
|
414
|
+
position: "absolute",
|
|
415
|
+
visibility: "hidden",
|
|
416
|
+
pointerEvents: "none",
|
|
417
|
+
width: "var(--fig-field-slider-elastic-distance)",
|
|
418
|
+
});
|
|
419
|
+
this.appendChild(probe);
|
|
420
|
+
raw = getComputedStyle(probe).width;
|
|
421
|
+
probe.remove();
|
|
422
|
+
}
|
|
423
|
+
const value = Number.parseFloat(raw);
|
|
424
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#updateElasticPull(pointerX) {
|
|
428
|
+
const rect = this.#elasticRangeRect;
|
|
429
|
+
if (!rect || !this.#elasticMaxPx) {
|
|
430
|
+
this.#resetElasticPull();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const overshoot =
|
|
434
|
+
pointerX < rect.left
|
|
435
|
+
? pointerX - rect.left
|
|
436
|
+
: pointerX > rect.right
|
|
437
|
+
? pointerX - rect.right
|
|
438
|
+
: 0;
|
|
439
|
+
if (!overshoot) {
|
|
440
|
+
this.#clearElasticPull();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const offset = Math.max(
|
|
444
|
+
-this.#elasticMaxPx,
|
|
445
|
+
Math.min(this.#elasticMaxPx, overshoot * 0.5),
|
|
446
|
+
);
|
|
447
|
+
const stretch = Math.abs(offset);
|
|
448
|
+
this.dataset.elasticDragging = "true";
|
|
449
|
+
this.style.setProperty("--fig-field-slider-elastic-size", `${stretch}px`);
|
|
450
|
+
this.style.setProperty(
|
|
451
|
+
"--fig-field-slider-elastic-position-offset",
|
|
452
|
+
offset < 0 ? `${-stretch / 2}px` : `${stretch / 2}px`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#resetElasticPull() {
|
|
457
|
+
this.#clearElasticPull();
|
|
458
|
+
this.#elasticMaxPx = 0;
|
|
459
|
+
this.#elasticRangeRect = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#clearElasticPull() {
|
|
463
|
+
this.removeAttribute("data-elastic-dragging");
|
|
464
|
+
this.style.removeProperty("--fig-field-slider-elastic-size");
|
|
465
|
+
this.style.removeProperty("--fig-field-slider-elastic-position-offset");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#valueFromPointer(event) {
|
|
469
|
+
const input = this.#rangeInput;
|
|
470
|
+
if (!input) return this.#slider?.value ?? "";
|
|
471
|
+
const rect = input.getBoundingClientRect();
|
|
472
|
+
const percent = rect.width
|
|
473
|
+
? Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width))
|
|
474
|
+
: 0;
|
|
475
|
+
const min = Number(input.min || 0);
|
|
476
|
+
const max = Number(input.max || 100);
|
|
477
|
+
const step = input.step === "any" ? 0 : Number(input.step || 1);
|
|
478
|
+
const raw = min + (max - min) * percent;
|
|
479
|
+
if (!step) return String(raw);
|
|
480
|
+
const snapped = Math.round((raw - min) / step) * step + min;
|
|
481
|
+
const decimals = Math.max(0, `${step}`.split(".")[1]?.length || 0);
|
|
482
|
+
return String(Number(snapped.toFixed(decimals)));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#defaultValue() {
|
|
486
|
+
return (
|
|
487
|
+
this.getAttribute("default") ??
|
|
488
|
+
this.#slider?.getAttribute("default") ??
|
|
489
|
+
this.getAttribute("value") ??
|
|
490
|
+
this.#slider?.getAttribute("value") ??
|
|
491
|
+
this.#rangeInput?.min ??
|
|
492
|
+
"0"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#resetToDefault() {
|
|
497
|
+
this.#clearPendingClick();
|
|
498
|
+
this.#setSliderValue(this.#defaultValue(), "input");
|
|
499
|
+
this.#setSliderValue(this.#defaultValue(), "change");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#setSliderValue(value, eventType) {
|
|
503
|
+
if (!this.#slider || value === null || value === undefined) return;
|
|
504
|
+
this.#slider.value = value;
|
|
505
|
+
this.dispatchEvent(
|
|
506
|
+
new CustomEvent(eventType, {
|
|
507
|
+
detail: this.#slider.value,
|
|
508
|
+
bubbles: true,
|
|
509
|
+
cancelable: true,
|
|
510
|
+
composed: true,
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
208
513
|
}
|
|
209
514
|
|
|
210
515
|
#syncSteppersToNumberInput() {
|
|
@@ -254,6 +559,9 @@ class FigFieldSlider extends HTMLElement {
|
|
|
254
559
|
|
|
255
560
|
#forwardSliderEvent(type, event) {
|
|
256
561
|
event.stopPropagation();
|
|
562
|
+
if (type === "change") {
|
|
563
|
+
this.#resetElasticPull();
|
|
564
|
+
}
|
|
257
565
|
const detail =
|
|
258
566
|
event instanceof CustomEvent && event.detail !== undefined
|
|
259
567
|
? event.detail
|
|
@@ -267,6 +575,14 @@ class FigFieldSlider extends HTMLElement {
|
|
|
267
575
|
}),
|
|
268
576
|
);
|
|
269
577
|
}
|
|
578
|
+
|
|
579
|
+
focus(options) {
|
|
580
|
+
this.#slider?.querySelector('input[type="range"]')?.focus(options);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
resetToDefault() {
|
|
584
|
+
this.#resetToDefault();
|
|
585
|
+
}
|
|
270
586
|
}
|
|
271
587
|
customElements.define("fig-field-slider", FigFieldSlider);
|
|
272
588
|
|