@rogieking/figui3 2.36.0 → 2.38.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 -1
- package/components.css +191 -4
- package/dist/fig.js +81 -52
- package/fig.js +557 -17
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -2357,6 +2357,7 @@ customElements.define("fig-segmented-control", FigSegmentedControl);
|
|
|
2357
2357
|
* @attr {number} max - The maximum value
|
|
2358
2358
|
* @attr {number} step - The step increment
|
|
2359
2359
|
* @attr {boolean} text - Whether to show a text input alongside the slider
|
|
2360
|
+
* @attr {string} placeholder - Placeholder for the number input when text is enabled
|
|
2360
2361
|
* @attr {string} units - The units to display after the value
|
|
2361
2362
|
* @attr {number} transform - A multiplier for the displayed value
|
|
2362
2363
|
* @attr {boolean} disabled - Whether the slider is disabled
|
|
@@ -2364,6 +2365,7 @@ customElements.define("fig-segmented-control", FigSegmentedControl);
|
|
|
2364
2365
|
*/
|
|
2365
2366
|
class FigSlider extends HTMLElement {
|
|
2366
2367
|
#isInteracting = false;
|
|
2368
|
+
#showEmptyTextValue = false;
|
|
2367
2369
|
// Private fields declarations
|
|
2368
2370
|
#typeDefaults = {
|
|
2369
2371
|
range: { min: 0, max: 100, step: 1 },
|
|
@@ -2405,7 +2407,7 @@ class FigSlider extends HTMLElement {
|
|
|
2405
2407
|
}
|
|
2406
2408
|
|
|
2407
2409
|
#regenerateInnerHTML() {
|
|
2408
|
-
|
|
2410
|
+
const rawValue = this.getAttribute("value");
|
|
2409
2411
|
this.type = this.getAttribute("type") || "range";
|
|
2410
2412
|
this.variant = this.getAttribute("variant") || "default";
|
|
2411
2413
|
this.text =
|
|
@@ -2416,13 +2418,25 @@ class FigSlider extends HTMLElement {
|
|
|
2416
2418
|
this.precision = this.hasAttribute("precision")
|
|
2417
2419
|
? Number(this.getAttribute("precision"))
|
|
2418
2420
|
: null;
|
|
2421
|
+
this.placeholder =
|
|
2422
|
+
this.getAttribute("placeholder") !== null
|
|
2423
|
+
? this.getAttribute("placeholder")
|
|
2424
|
+
: "##";
|
|
2419
2425
|
|
|
2420
2426
|
const defaults = this.#typeDefaults[this.type];
|
|
2421
2427
|
this.min = Number(this.getAttribute("min") || defaults.min);
|
|
2422
2428
|
this.max = Number(this.getAttribute("max") || defaults.max);
|
|
2423
2429
|
this.step = Number(this.getAttribute("step") || defaults.step);
|
|
2424
2430
|
this.color = this.getAttribute("color") || defaults?.color;
|
|
2425
|
-
this.default = this.
|
|
2431
|
+
this.default = this.hasAttribute("default")
|
|
2432
|
+
? this.getAttribute("default")
|
|
2433
|
+
: this.type === "delta"
|
|
2434
|
+
? 0
|
|
2435
|
+
: this.min;
|
|
2436
|
+
this.#showEmptyTextValue =
|
|
2437
|
+
rawValue === null ||
|
|
2438
|
+
(typeof rawValue === "string" && rawValue.trim() === "");
|
|
2439
|
+
this.value = this.#normalizeSliderValue(rawValue);
|
|
2426
2440
|
|
|
2427
2441
|
if (this.color) {
|
|
2428
2442
|
this.style.setProperty("--color", this.color);
|
|
@@ -2447,12 +2461,12 @@ class FigSlider extends HTMLElement {
|
|
|
2447
2461
|
if (this.text) {
|
|
2448
2462
|
html = `${slider}
|
|
2449
2463
|
<fig-input-number
|
|
2450
|
-
placeholder="
|
|
2464
|
+
placeholder="${this.placeholder}"
|
|
2451
2465
|
min="${this.min}"
|
|
2452
2466
|
max="${this.max}"
|
|
2453
2467
|
transform="${this.transform}"
|
|
2454
2468
|
step="${this.step}"
|
|
2455
|
-
value="${this.value}"
|
|
2469
|
+
value="${this.#showEmptyTextValue ? "" : this.value}"
|
|
2456
2470
|
${this.units ? `units="${this.units}"` : ""}
|
|
2457
2471
|
${this.precision !== null ? `precision="${this.precision}"` : ""}>
|
|
2458
2472
|
</fig-input-number>`;
|
|
@@ -2566,17 +2580,59 @@ class FigSlider extends HTMLElement {
|
|
|
2566
2580
|
|
|
2567
2581
|
#handleTextInput() {
|
|
2568
2582
|
if (this.figInputNumber) {
|
|
2569
|
-
|
|
2570
|
-
this.#
|
|
2583
|
+
const rawTextValue = this.figInputNumber.value;
|
|
2584
|
+
this.#showEmptyTextValue =
|
|
2585
|
+
rawTextValue === null ||
|
|
2586
|
+
rawTextValue === undefined ||
|
|
2587
|
+
(typeof rawTextValue === "string" && rawTextValue.trim() === "");
|
|
2588
|
+
const normalized = this.#normalizeSliderValue(rawTextValue);
|
|
2589
|
+
this.value = normalized;
|
|
2590
|
+
this.input.value = String(normalized);
|
|
2591
|
+
this.#syncValue();
|
|
2571
2592
|
this.dispatchEvent(
|
|
2572
2593
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
2573
2594
|
);
|
|
2574
2595
|
}
|
|
2575
2596
|
}
|
|
2576
2597
|
#calculateNormal(value) {
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2598
|
+
const { min, max } = this.#getBounds();
|
|
2599
|
+
const range = max - min;
|
|
2600
|
+
if (range === 0) return 0;
|
|
2601
|
+
return (Number(value) - min) / range;
|
|
2602
|
+
}
|
|
2603
|
+
#toFiniteNumber(value) {
|
|
2604
|
+
if (value === null || value === undefined) return null;
|
|
2605
|
+
if (typeof value === "string" && value.trim() === "") return null;
|
|
2606
|
+
const parsed = Number(value);
|
|
2607
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2608
|
+
}
|
|
2609
|
+
#getBounds() {
|
|
2610
|
+
let min = this.#toFiniteNumber(this.min);
|
|
2611
|
+
let max = this.#toFiniteNumber(this.max);
|
|
2612
|
+
if (min === null) min = 0;
|
|
2613
|
+
if (max === null) max = min;
|
|
2614
|
+
if (min > max) {
|
|
2615
|
+
[min, max] = [max, min];
|
|
2616
|
+
}
|
|
2617
|
+
return { min, max };
|
|
2618
|
+
}
|
|
2619
|
+
#clampToBounds(value) {
|
|
2620
|
+
const { min, max } = this.#getBounds();
|
|
2621
|
+
return Math.min(max, Math.max(min, value));
|
|
2622
|
+
}
|
|
2623
|
+
#getFallbackValue() {
|
|
2624
|
+
if (this.type === "delta") {
|
|
2625
|
+
const deltaDefault = this.#toFiniteNumber(this.default);
|
|
2626
|
+
if (deltaDefault !== null) return this.#clampToBounds(deltaDefault);
|
|
2627
|
+
return this.#clampToBounds(0);
|
|
2628
|
+
}
|
|
2629
|
+
const { min } = this.#getBounds();
|
|
2630
|
+
return min;
|
|
2631
|
+
}
|
|
2632
|
+
#normalizeSliderValue(rawValue) {
|
|
2633
|
+
const parsed = this.#toFiniteNumber(rawValue);
|
|
2634
|
+
if (parsed === null) return this.#getFallbackValue();
|
|
2635
|
+
return this.#clampToBounds(parsed);
|
|
2580
2636
|
}
|
|
2581
2637
|
#syncProperties() {
|
|
2582
2638
|
let complete = this.#calculateNormal(this.value);
|
|
@@ -2592,11 +2648,15 @@ class FigSlider extends HTMLElement {
|
|
|
2592
2648
|
// Update ARIA value
|
|
2593
2649
|
this.input.setAttribute("aria-valuenow", val);
|
|
2594
2650
|
if (this.figInputNumber) {
|
|
2595
|
-
this.figInputNumber.setAttribute(
|
|
2651
|
+
this.figInputNumber.setAttribute(
|
|
2652
|
+
"value",
|
|
2653
|
+
this.#showEmptyTextValue ? "" : val,
|
|
2654
|
+
);
|
|
2596
2655
|
}
|
|
2597
2656
|
}
|
|
2598
2657
|
|
|
2599
2658
|
#handleInput() {
|
|
2659
|
+
this.#showEmptyTextValue = false;
|
|
2600
2660
|
this.#syncValue();
|
|
2601
2661
|
this.dispatchEvent(
|
|
2602
2662
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
@@ -2605,6 +2665,7 @@ class FigSlider extends HTMLElement {
|
|
|
2605
2665
|
|
|
2606
2666
|
#handleChange() {
|
|
2607
2667
|
this.#isInteracting = false;
|
|
2668
|
+
this.#showEmptyTextValue = false;
|
|
2608
2669
|
this.#syncValue();
|
|
2609
2670
|
this.dispatchEvent(
|
|
2610
2671
|
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
@@ -2613,8 +2674,15 @@ class FigSlider extends HTMLElement {
|
|
|
2613
2674
|
|
|
2614
2675
|
#handleTextChange() {
|
|
2615
2676
|
if (this.figInputNumber) {
|
|
2616
|
-
|
|
2617
|
-
this.#
|
|
2677
|
+
const rawTextValue = this.figInputNumber.value;
|
|
2678
|
+
this.#showEmptyTextValue =
|
|
2679
|
+
rawTextValue === null ||
|
|
2680
|
+
rawTextValue === undefined ||
|
|
2681
|
+
(typeof rawTextValue === "string" && rawTextValue.trim() === "");
|
|
2682
|
+
const normalized = this.#normalizeSliderValue(rawTextValue);
|
|
2683
|
+
this.value = normalized;
|
|
2684
|
+
this.input.value = String(normalized);
|
|
2685
|
+
this.#syncValue();
|
|
2618
2686
|
this.dispatchEvent(
|
|
2619
2687
|
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
2620
2688
|
);
|
|
@@ -2634,6 +2702,7 @@ class FigSlider extends HTMLElement {
|
|
|
2634
2702
|
"units",
|
|
2635
2703
|
"transform",
|
|
2636
2704
|
"text",
|
|
2705
|
+
"placeholder",
|
|
2637
2706
|
"default",
|
|
2638
2707
|
"precision",
|
|
2639
2708
|
];
|
|
@@ -2660,11 +2729,17 @@ class FigSlider extends HTMLElement {
|
|
|
2660
2729
|
break;
|
|
2661
2730
|
case "value":
|
|
2662
2731
|
if (this.#isInteracting) break;
|
|
2663
|
-
this
|
|
2664
|
-
|
|
2665
|
-
|
|
2732
|
+
this.#showEmptyTextValue =
|
|
2733
|
+
newValue === null ||
|
|
2734
|
+
(typeof newValue === "string" && newValue.trim() === "");
|
|
2735
|
+
this.value = this.#normalizeSliderValue(newValue);
|
|
2736
|
+
this.input.value = String(this.value);
|
|
2737
|
+
this.#syncValue();
|
|
2666
2738
|
if (this.figInputNumber) {
|
|
2667
|
-
this.figInputNumber.setAttribute(
|
|
2739
|
+
this.figInputNumber.setAttribute(
|
|
2740
|
+
"value",
|
|
2741
|
+
this.#showEmptyTextValue ? "" : this.value,
|
|
2742
|
+
);
|
|
2668
2743
|
}
|
|
2669
2744
|
break;
|
|
2670
2745
|
case "transform":
|
|
@@ -2683,8 +2758,19 @@ class FigSlider extends HTMLElement {
|
|
|
2683
2758
|
}
|
|
2684
2759
|
}
|
|
2685
2760
|
break;
|
|
2761
|
+
case "placeholder":
|
|
2762
|
+
this.placeholder = newValue !== null ? newValue : "##";
|
|
2763
|
+
if (this.figInputNumber) {
|
|
2764
|
+
this.figInputNumber.setAttribute("placeholder", this.placeholder);
|
|
2765
|
+
}
|
|
2766
|
+
break;
|
|
2686
2767
|
case "default":
|
|
2687
|
-
this.default =
|
|
2768
|
+
this.default =
|
|
2769
|
+
newValue !== null
|
|
2770
|
+
? newValue
|
|
2771
|
+
: this.type === "delta"
|
|
2772
|
+
? 0
|
|
2773
|
+
: this.min;
|
|
2688
2774
|
this.#syncProperties();
|
|
2689
2775
|
break;
|
|
2690
2776
|
case "min":
|
|
@@ -6772,6 +6858,460 @@ class Fig3DRotate extends HTMLElement {
|
|
|
6772
6858
|
}
|
|
6773
6859
|
customElements.define("fig-3d-rotate", Fig3DRotate);
|
|
6774
6860
|
|
|
6861
|
+
/**
|
|
6862
|
+
* A transform-origin grid control with draggable handle.
|
|
6863
|
+
* @attr {string} value - CSS transform-origin pair, e.g. "50% 50%".
|
|
6864
|
+
* @attr {number} precision - Decimal places for percentage output (default 0).
|
|
6865
|
+
* @attr {boolean} drag - Enable handle dragging (default true).
|
|
6866
|
+
* @attr {boolean} fields - Show X/Y percentage fields when present/true (default false).
|
|
6867
|
+
*/
|
|
6868
|
+
class FigOriginGrid extends HTMLElement {
|
|
6869
|
+
#x = 50;
|
|
6870
|
+
#y = 50;
|
|
6871
|
+
#precision = 0;
|
|
6872
|
+
#grid = null;
|
|
6873
|
+
#cells = [];
|
|
6874
|
+
#handle = null;
|
|
6875
|
+
#xInput = null;
|
|
6876
|
+
#yInput = null;
|
|
6877
|
+
#isDragging = false;
|
|
6878
|
+
#isSyncingValueAttr = false;
|
|
6879
|
+
#activePointerId = null;
|
|
6880
|
+
#boundHandlePointerMove = null;
|
|
6881
|
+
#boundHandlePointerEnd = null;
|
|
6882
|
+
|
|
6883
|
+
static SNAP_POINTS = [0, 16.6667, 33.3333, 50, 66.6667, 83.3333, 100];
|
|
6884
|
+
|
|
6885
|
+
static get observedAttributes() {
|
|
6886
|
+
return ["value", "precision", "aspect-ratio", "drag", "fields"];
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6889
|
+
connectedCallback() {
|
|
6890
|
+
this.#precision = parseInt(this.getAttribute("precision") || "0");
|
|
6891
|
+
this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
|
|
6892
|
+
this.#applyIncomingValue(this.getAttribute("value"));
|
|
6893
|
+
|
|
6894
|
+
this.#render();
|
|
6895
|
+
this.#syncDragState();
|
|
6896
|
+
this.#syncValueAttribute();
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
disconnectedCallback() {
|
|
6900
|
+
this.#isDragging = false;
|
|
6901
|
+
this.#detachHandleDragListeners();
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6904
|
+
get value() {
|
|
6905
|
+
const p = this.#precision;
|
|
6906
|
+
return `${this.#x.toFixed(p)}% ${this.#y.toFixed(p)}%`;
|
|
6907
|
+
}
|
|
6908
|
+
|
|
6909
|
+
set value(v) {
|
|
6910
|
+
this.setAttribute("value", v);
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
6914
|
+
if (name === "aspect-ratio") {
|
|
6915
|
+
this.#syncAspectRatioVar(newValue);
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
if (name === "drag") {
|
|
6919
|
+
this.#syncDragState();
|
|
6920
|
+
return;
|
|
6921
|
+
}
|
|
6922
|
+
if (name === "fields") {
|
|
6923
|
+
this.#render();
|
|
6924
|
+
this.#syncDragState();
|
|
6925
|
+
this.#syncValueAttribute();
|
|
6926
|
+
return;
|
|
6927
|
+
}
|
|
6928
|
+
if (name === "precision") {
|
|
6929
|
+
this.#precision = parseInt(newValue || "0");
|
|
6930
|
+
this.#syncValueInputs();
|
|
6931
|
+
this.#syncValueAttribute();
|
|
6932
|
+
return;
|
|
6933
|
+
}
|
|
6934
|
+
if (name === "value") {
|
|
6935
|
+
if (this.#isSyncingValueAttr || this.#isDragging) return;
|
|
6936
|
+
this.#applyIncomingValue(newValue);
|
|
6937
|
+
this.#syncHandlePosition();
|
|
6938
|
+
this.#syncOverflowState();
|
|
6939
|
+
this.#syncValueInputs();
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
get #dragEnabled() {
|
|
6944
|
+
const attr = this.getAttribute("drag");
|
|
6945
|
+
return attr === null || attr.toLowerCase() !== "false";
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6948
|
+
get #fieldsEnabled() {
|
|
6949
|
+
const attr = this.getAttribute("fields");
|
|
6950
|
+
if (attr === null) return false;
|
|
6951
|
+
return attr.toLowerCase() !== "false";
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
#syncAspectRatioVar(value) {
|
|
6955
|
+
if (value && value.trim()) {
|
|
6956
|
+
this.style.setProperty("--aspect-ratio", value.trim());
|
|
6957
|
+
} else {
|
|
6958
|
+
this.style.removeProperty("--aspect-ratio");
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
|
|
6962
|
+
#syncDragState() {
|
|
6963
|
+
if (!this.#grid) return;
|
|
6964
|
+
this.#grid.classList.toggle("drag-disabled", !this.#dragEnabled);
|
|
6965
|
+
}
|
|
6966
|
+
|
|
6967
|
+
#clampPercentage(value) {
|
|
6968
|
+
return Math.max(0, Math.min(100, value));
|
|
6969
|
+
}
|
|
6970
|
+
|
|
6971
|
+
#parseAxisValue(raw, axis) {
|
|
6972
|
+
const token = (raw || "").trim().toLowerCase();
|
|
6973
|
+
if (!token) return axis === "x" ? this.#x : this.#y;
|
|
6974
|
+
|
|
6975
|
+
const keywordMap =
|
|
6976
|
+
axis === "x"
|
|
6977
|
+
? { left: 0, center: 50, right: 100 }
|
|
6978
|
+
: { top: 0, center: 50, bottom: 100 };
|
|
6979
|
+
if (token in keywordMap) return keywordMap[token];
|
|
6980
|
+
|
|
6981
|
+
const numeric = Number.parseFloat(token.replace("%", ""));
|
|
6982
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
6983
|
+
|
|
6984
|
+
return axis === "x" ? this.#x : this.#y;
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6987
|
+
#parseValue(value) {
|
|
6988
|
+
const parts = value
|
|
6989
|
+
.trim()
|
|
6990
|
+
.replace(/,/g, " ")
|
|
6991
|
+
.split(/\s+/)
|
|
6992
|
+
.filter(Boolean);
|
|
6993
|
+
if (parts.length < 1) return;
|
|
6994
|
+
|
|
6995
|
+
if (parts.length === 1) {
|
|
6996
|
+
const same = this.#parseAxisValue(parts[0], "x");
|
|
6997
|
+
this.#x = same;
|
|
6998
|
+
this.#y = same;
|
|
6999
|
+
return;
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
this.#x = this.#parseAxisValue(parts[0], "x");
|
|
7003
|
+
this.#y = this.#parseAxisValue(parts[1], "y");
|
|
7004
|
+
}
|
|
7005
|
+
|
|
7006
|
+
#applyIncomingValue(value) {
|
|
7007
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
7008
|
+
if (!normalized) {
|
|
7009
|
+
this.#x = 50;
|
|
7010
|
+
this.#y = 50;
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
this.#parseValue(normalized);
|
|
7014
|
+
}
|
|
7015
|
+
|
|
7016
|
+
#render() {
|
|
7017
|
+
const cells = Array.from({ length: 9 }, (_, index) => {
|
|
7018
|
+
const col = index % 3;
|
|
7019
|
+
const row = Math.floor(index / 3);
|
|
7020
|
+
return `<span class="origin-grid-cell" data-col="${col}" data-row="${row}">
|
|
7021
|
+
<span class="origin-grid-dot"></span>
|
|
7022
|
+
</span>`;
|
|
7023
|
+
}).join("");
|
|
7024
|
+
|
|
7025
|
+
const xValue = this.#x.toFixed(this.#precision);
|
|
7026
|
+
const yValue = this.#y.toFixed(this.#precision);
|
|
7027
|
+
const fieldsMarkup = this.#fieldsEnabled
|
|
7028
|
+
? `<div class="origin-values">
|
|
7029
|
+
<fig-input-number name="x" value="${xValue}" step="1" units="%"><span slot="prepend">X</span></fig-input-number>
|
|
7030
|
+
<fig-input-number name="y" value="${yValue}" step="1" units="%"><span slot="prepend">Y</span></fig-input-number>
|
|
7031
|
+
</div>`
|
|
7032
|
+
: "";
|
|
7033
|
+
|
|
7034
|
+
this.innerHTML = `<div class="fig-origin-grid-surface">
|
|
7035
|
+
<div class="origin-grid" aria-label="Transform origin grid">
|
|
7036
|
+
<div class="origin-grid-cells">${cells}</div>
|
|
7037
|
+
<span class="origin-grid-handle"></span>
|
|
7038
|
+
</div>
|
|
7039
|
+
</div>
|
|
7040
|
+
${fieldsMarkup}`;
|
|
7041
|
+
|
|
7042
|
+
this.#grid = this.querySelector(".origin-grid");
|
|
7043
|
+
this.#cells = Array.from(this.querySelectorAll(".origin-grid-cell"));
|
|
7044
|
+
this.#handle = this.querySelector(".origin-grid-handle");
|
|
7045
|
+
this.#xInput = this.querySelector('fig-input-number[name="x"]');
|
|
7046
|
+
this.#yInput = this.querySelector('fig-input-number[name="y"]');
|
|
7047
|
+
this.#syncHandlePosition();
|
|
7048
|
+
this.#syncOverflowState();
|
|
7049
|
+
this.#syncValueInputs();
|
|
7050
|
+
this.#setupEvents();
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
#syncValueInputs() {
|
|
7054
|
+
const xValue = this.#x.toFixed(this.#precision);
|
|
7055
|
+
const yValue = this.#y.toFixed(this.#precision);
|
|
7056
|
+
if (this.#xInput) {
|
|
7057
|
+
this.#xInput.setAttribute("value", xValue);
|
|
7058
|
+
}
|
|
7059
|
+
if (this.#yInput) {
|
|
7060
|
+
this.#yInput.setAttribute("value", yValue);
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
|
|
7064
|
+
#syncValueAttribute() {
|
|
7065
|
+
const next = this.value;
|
|
7066
|
+
if (this.getAttribute("value") === next) return;
|
|
7067
|
+
this.#isSyncingValueAttr = true;
|
|
7068
|
+
this.setAttribute("value", next);
|
|
7069
|
+
this.#isSyncingValueAttr = false;
|
|
7070
|
+
}
|
|
7071
|
+
|
|
7072
|
+
#emit(type) {
|
|
7073
|
+
this.dispatchEvent(
|
|
7074
|
+
new CustomEvent(type, {
|
|
7075
|
+
bubbles: true,
|
|
7076
|
+
detail: {
|
|
7077
|
+
value: this.value,
|
|
7078
|
+
x: this.#x,
|
|
7079
|
+
y: this.#y,
|
|
7080
|
+
},
|
|
7081
|
+
}),
|
|
7082
|
+
);
|
|
7083
|
+
}
|
|
7084
|
+
|
|
7085
|
+
#syncHandlePosition() {
|
|
7086
|
+
if (!this.#handle) return;
|
|
7087
|
+
// Constrain draggable visual bounds to the 3x3 dot centers.
|
|
7088
|
+
const toVisual = (value) => 16.6667 + (this.#clampPercentage(value) / 100) * 66.6667;
|
|
7089
|
+
this.#handle.style.left = `${toVisual(this.#x)}%`;
|
|
7090
|
+
this.#handle.style.top = `${toVisual(this.#y)}%`;
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
#syncOverflowState() {
|
|
7094
|
+
if (!this.#handle) return;
|
|
7095
|
+
const overflowX = this.#x < 0 || this.#x > 100;
|
|
7096
|
+
const overflowY = this.#y < 0 || this.#y > 100;
|
|
7097
|
+
const overflowLeft = this.#x < 0;
|
|
7098
|
+
const overflowRight = this.#x > 100;
|
|
7099
|
+
const overflowUp = this.#y < 0;
|
|
7100
|
+
const overflowDown = this.#y > 100;
|
|
7101
|
+
|
|
7102
|
+
this.#handle.classList.toggle("beyond-bounds-x", overflowX);
|
|
7103
|
+
this.#handle.classList.toggle("beyond-bounds-y", overflowY);
|
|
7104
|
+
this.#handle.classList.toggle("overflow-left", overflowLeft);
|
|
7105
|
+
this.#handle.classList.toggle("overflow-right", overflowRight);
|
|
7106
|
+
this.#handle.classList.toggle("overflow-up", overflowUp);
|
|
7107
|
+
this.#handle.classList.toggle("overflow-down", overflowDown);
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7110
|
+
#gridCellFromClient(clientX, clientY) {
|
|
7111
|
+
if (!this.#grid) return null;
|
|
7112
|
+
const rect = this.#grid.getBoundingClientRect();
|
|
7113
|
+
const nx = (clientX - rect.left) / Math.max(rect.width, 1);
|
|
7114
|
+
const ny = (clientY - rect.top) / Math.max(rect.height, 1);
|
|
7115
|
+
if (nx < 0 || nx > 1 || ny < 0 || ny > 1) return null;
|
|
7116
|
+
const col = Math.max(0, Math.min(2, Math.floor(nx * 3)));
|
|
7117
|
+
const row = Math.max(0, Math.min(2, Math.floor(ny * 3)));
|
|
7118
|
+
return this.#cells.find(
|
|
7119
|
+
(cell) =>
|
|
7120
|
+
Number(cell.getAttribute("data-col")) === col &&
|
|
7121
|
+
Number(cell.getAttribute("data-row")) === row,
|
|
7122
|
+
);
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7125
|
+
#clearHoveredCells() {
|
|
7126
|
+
for (const cell of this.#cells) {
|
|
7127
|
+
cell.classList.remove("is-hovered");
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
|
|
7131
|
+
#setHoveredCell(cell) {
|
|
7132
|
+
this.#clearHoveredCells();
|
|
7133
|
+
if (cell) cell.classList.add("is-hovered");
|
|
7134
|
+
}
|
|
7135
|
+
|
|
7136
|
+
#setFromPercent(x, y, eventType) {
|
|
7137
|
+
const nextX = Number(x);
|
|
7138
|
+
const nextY = Number(y);
|
|
7139
|
+
if (!Number.isFinite(nextX) || !Number.isFinite(nextY)) return;
|
|
7140
|
+
if (nextX === this.#x && nextY === this.#y && eventType === "input") return;
|
|
7141
|
+
|
|
7142
|
+
this.#x = nextX;
|
|
7143
|
+
this.#y = nextY;
|
|
7144
|
+
this.#syncHandlePosition();
|
|
7145
|
+
this.#syncOverflowState();
|
|
7146
|
+
this.#syncValueInputs();
|
|
7147
|
+
this.#syncValueAttribute();
|
|
7148
|
+
this.#emit(eventType);
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
#clientToPercent(clientX, clientY) {
|
|
7152
|
+
if (!this.#grid) return { x: this.#x, y: this.#y };
|
|
7153
|
+
const rect = this.#grid.getBoundingClientRect();
|
|
7154
|
+
const insetX = rect.width / 6;
|
|
7155
|
+
const insetY = rect.height / 6;
|
|
7156
|
+
const usableWidth = Math.max(1, rect.width - insetX * 2);
|
|
7157
|
+
const usableHeight = Math.max(1, rect.height - insetY * 2);
|
|
7158
|
+
const nx = (clientX - (rect.left + insetX)) / usableWidth;
|
|
7159
|
+
const ny = (clientY - (rect.top + insetY)) / usableHeight;
|
|
7160
|
+
return {
|
|
7161
|
+
x: nx * 100,
|
|
7162
|
+
y: ny * 100,
|
|
7163
|
+
};
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
#cellCenterFromClient(clientX, clientY) {
|
|
7167
|
+
if (!this.#grid) return { x: 50, y: 50 };
|
|
7168
|
+
const rect = this.#grid.getBoundingClientRect();
|
|
7169
|
+
const colRaw = ((clientX - rect.left) / Math.max(rect.width, 1)) * 3;
|
|
7170
|
+
const rowRaw = ((clientY - rect.top) / Math.max(rect.height, 1)) * 3;
|
|
7171
|
+
const col = Math.max(0, Math.min(2, Math.floor(colRaw)));
|
|
7172
|
+
const row = Math.max(0, Math.min(2, Math.floor(rowRaw)));
|
|
7173
|
+
return {
|
|
7174
|
+
x: col * 50,
|
|
7175
|
+
y: row * 50,
|
|
7176
|
+
};
|
|
7177
|
+
}
|
|
7178
|
+
|
|
7179
|
+
#snapPercentage(value) {
|
|
7180
|
+
const threshold = 2.5;
|
|
7181
|
+
let closest = value;
|
|
7182
|
+
let closestDistance = Infinity;
|
|
7183
|
+
for (const point of FigOriginGrid.SNAP_POINTS) {
|
|
7184
|
+
const distance = Math.abs(value - point);
|
|
7185
|
+
if (distance < closestDistance) {
|
|
7186
|
+
closestDistance = distance;
|
|
7187
|
+
closest = point;
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
return closestDistance <= threshold ? closest : value;
|
|
7191
|
+
}
|
|
7192
|
+
|
|
7193
|
+
#maybeSnapPoint(point, shiftKey) {
|
|
7194
|
+
if (!shiftKey) return point;
|
|
7195
|
+
return {
|
|
7196
|
+
x: this.#snapPercentage(point.x),
|
|
7197
|
+
y: this.#snapPercentage(point.y),
|
|
7198
|
+
};
|
|
7199
|
+
}
|
|
7200
|
+
|
|
7201
|
+
#detachHandleDragListeners() {
|
|
7202
|
+
if (!this.#grid || !this.#boundHandlePointerMove || !this.#boundHandlePointerEnd) return;
|
|
7203
|
+
this.#grid.removeEventListener("pointermove", this.#boundHandlePointerMove);
|
|
7204
|
+
this.#grid.removeEventListener("pointerup", this.#boundHandlePointerEnd);
|
|
7205
|
+
this.#grid.removeEventListener("pointercancel", this.#boundHandlePointerEnd);
|
|
7206
|
+
this.#grid.removeEventListener(
|
|
7207
|
+
"lostpointercapture",
|
|
7208
|
+
this.#boundHandlePointerEnd,
|
|
7209
|
+
);
|
|
7210
|
+
this.#boundHandlePointerMove = null;
|
|
7211
|
+
this.#boundHandlePointerEnd = null;
|
|
7212
|
+
}
|
|
7213
|
+
|
|
7214
|
+
#startGridDrag(e) {
|
|
7215
|
+
if (!this.#grid || !this.#dragEnabled) return;
|
|
7216
|
+
e.preventDefault();
|
|
7217
|
+
this.#isDragging = true;
|
|
7218
|
+
this.#activePointerId = e.pointerId;
|
|
7219
|
+
const startClientX = e.clientX;
|
|
7220
|
+
const startClientY = e.clientY;
|
|
7221
|
+
const dragThresholdPx = 3;
|
|
7222
|
+
let didDrag = false;
|
|
7223
|
+
this.#grid.setPointerCapture(e.pointerId);
|
|
7224
|
+
|
|
7225
|
+
this.#boundHandlePointerMove = (moveEvent) => {
|
|
7226
|
+
if (!this.#isDragging || moveEvent.pointerId !== this.#activePointerId) return;
|
|
7227
|
+
const dx = moveEvent.clientX - startClientX;
|
|
7228
|
+
const dy = moveEvent.clientY - startClientY;
|
|
7229
|
+
const distance = Math.hypot(dx, dy);
|
|
7230
|
+
if (!didDrag && distance < dragThresholdPx) return;
|
|
7231
|
+
if (!didDrag) {
|
|
7232
|
+
didDrag = true;
|
|
7233
|
+
this.#grid.classList.add("is-dragging");
|
|
7234
|
+
this.#clearHoveredCells();
|
|
7235
|
+
}
|
|
7236
|
+
const nextPoint = this.#maybeSnapPoint(
|
|
7237
|
+
this.#clientToPercent(moveEvent.clientX, moveEvent.clientY),
|
|
7238
|
+
moveEvent.shiftKey,
|
|
7239
|
+
);
|
|
7240
|
+
this.#setFromPercent(nextPoint.x, nextPoint.y, "input");
|
|
7241
|
+
};
|
|
7242
|
+
|
|
7243
|
+
this.#boundHandlePointerEnd = (endEvent) => {
|
|
7244
|
+
if (!this.#isDragging || endEvent.pointerId !== this.#activePointerId) return;
|
|
7245
|
+
this.#isDragging = false;
|
|
7246
|
+
this.#activePointerId = null;
|
|
7247
|
+
this.#grid.classList.remove("is-dragging");
|
|
7248
|
+
this.#clearHoveredCells();
|
|
7249
|
+
this.#detachHandleDragListeners();
|
|
7250
|
+
if (!didDrag) {
|
|
7251
|
+
// Click behavior: snap to the center of the clicked cell.
|
|
7252
|
+
const center = this.#cellCenterFromClient(startClientX, startClientY);
|
|
7253
|
+
this.#setFromPercent(center.x, center.y, "input");
|
|
7254
|
+
}
|
|
7255
|
+
this.#emit("change");
|
|
7256
|
+
};
|
|
7257
|
+
|
|
7258
|
+
this.#grid.addEventListener("pointermove", this.#boundHandlePointerMove);
|
|
7259
|
+
this.#grid.addEventListener("pointerup", this.#boundHandlePointerEnd);
|
|
7260
|
+
this.#grid.addEventListener("pointercancel", this.#boundHandlePointerEnd);
|
|
7261
|
+
this.#grid.addEventListener("lostpointercapture", this.#boundHandlePointerEnd);
|
|
7262
|
+
}
|
|
7263
|
+
|
|
7264
|
+
#setupEvents() {
|
|
7265
|
+
if (!this.#grid || !this.#handle) return;
|
|
7266
|
+
|
|
7267
|
+
this.#grid.addEventListener("pointerdown", (e) => {
|
|
7268
|
+
const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
|
|
7269
|
+
this.#setHoveredCell(hovered);
|
|
7270
|
+
|
|
7271
|
+
if (this.#dragEnabled) {
|
|
7272
|
+
this.#startGridDrag(e);
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
const center = this.#cellCenterFromClient(e.clientX, e.clientY);
|
|
7277
|
+
this.#setFromPercent(center.x, center.y, "input");
|
|
7278
|
+
this.#emit("change");
|
|
7279
|
+
});
|
|
7280
|
+
|
|
7281
|
+
this.#grid.addEventListener("pointermove", (e) => {
|
|
7282
|
+
if (this.#isDragging) return;
|
|
7283
|
+
const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
|
|
7284
|
+
this.#setHoveredCell(hovered);
|
|
7285
|
+
});
|
|
7286
|
+
|
|
7287
|
+
this.#grid.addEventListener("pointerleave", () => {
|
|
7288
|
+
this.#clearHoveredCells();
|
|
7289
|
+
});
|
|
7290
|
+
|
|
7291
|
+
const bindValueInput = (inputEl, axis) => {
|
|
7292
|
+
if (!inputEl) return;
|
|
7293
|
+
const handle = (e) => {
|
|
7294
|
+
const next = Number.parseFloat(e.target.value);
|
|
7295
|
+
if (!Number.isFinite(next)) return;
|
|
7296
|
+
if (axis === "x") {
|
|
7297
|
+
this.#setFromPercent(next, this.#y, "input");
|
|
7298
|
+
} else {
|
|
7299
|
+
this.#setFromPercent(this.#x, next, "input");
|
|
7300
|
+
}
|
|
7301
|
+
};
|
|
7302
|
+
inputEl.addEventListener("input", handle);
|
|
7303
|
+
inputEl.addEventListener("change", handle);
|
|
7304
|
+
inputEl.addEventListener("focusout", () => {
|
|
7305
|
+
this.#emit("change");
|
|
7306
|
+
});
|
|
7307
|
+
};
|
|
7308
|
+
|
|
7309
|
+
bindValueInput(this.#xInput, "x");
|
|
7310
|
+
bindValueInput(this.#yInput, "y");
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
customElements.define("fig-origin-grid", FigOriginGrid);
|
|
7314
|
+
|
|
6775
7315
|
/**
|
|
6776
7316
|
* A custom joystick input element.
|
|
6777
7317
|
* @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
|
package/package.json
CHANGED