@rogieking/figui3 8.10.0 → 8.11.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/.cursor/skills/fig-lab/SKILL.md +5 -2
- package/.cursor/skills/fig-lab/components.md +56 -1
- package/.cursor/skills/fig-lab/reference.md +57 -4
- package/.cursor/skills/figui3/components.md +1 -1
- package/.cursor/skills/figui3/react.md +2 -0
- package/.cursor/skills/figui3/reference.md +7 -0
- package/.cursor/skills/propkit/SKILL.md +6 -1
- package/README.md +96 -0
- package/components.css +22 -6
- package/dist/components.css +1 -1
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +3 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +1 -1
- package/fig-lab.css +355 -9
- package/fig-lab.js +1422 -129
- package/fig.js +5 -1
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -173,10 +173,19 @@ function figLabPropskitEventDetail(host, value = host.value) {
|
|
|
173
173
|
return detail;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function figLabDispatchPropskitEvent(
|
|
176
|
+
function figLabDispatchPropskitEvent(
|
|
177
|
+
host,
|
|
178
|
+
type,
|
|
179
|
+
value = host.value,
|
|
180
|
+
additionalDetail = null,
|
|
181
|
+
) {
|
|
182
|
+
const detail = figLabPropskitEventDetail(host, value);
|
|
183
|
+
if (additionalDetail && typeof additionalDetail === "object") {
|
|
184
|
+
Object.assign(detail, additionalDetail);
|
|
185
|
+
}
|
|
177
186
|
host.dispatchEvent(
|
|
178
187
|
new CustomEvent(type, {
|
|
179
|
-
detail
|
|
188
|
+
detail,
|
|
180
189
|
bubbles: true,
|
|
181
190
|
cancelable: true,
|
|
182
191
|
composed: true,
|
|
@@ -2579,203 +2588,1033 @@ class PropskitSelect extends FigLabPropskitElement {
|
|
|
2579
2588
|
}
|
|
2580
2589
|
figLabDefineElement("propskit-select", PropskitSelect);
|
|
2581
2590
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2591
|
+
/**
|
|
2592
|
+
* Compact selectable list with built-in add, rename, and delete actions.
|
|
2593
|
+
*
|
|
2594
|
+
* Options accept the same comma, newline, or JSON formats as fig-select.
|
|
2595
|
+
* Mutations normalize the options attribute to { value, label } objects so
|
|
2596
|
+
* labels can be renamed without changing stable option values.
|
|
2597
|
+
*
|
|
2598
|
+
* @attr {string} options - Select choices.
|
|
2599
|
+
* @attr {string} value - Selected option value.
|
|
2600
|
+
* @attr {string} default - Reset option value.
|
|
2601
|
+
* @attr {string} aria-label - Accessible control label.
|
|
2602
|
+
* @attr {boolean|string} disabled - Disables selection and list mutations.
|
|
2603
|
+
* @fires input - Shared PropsKit event with selected value and label.
|
|
2604
|
+
* @fires change - Shared PropsKit event with selected value and label.
|
|
2605
|
+
* @fires optionhover - Shared PropsKit event with hovered value and label.
|
|
2606
|
+
*/
|
|
2607
|
+
class PropskitEditableSelect extends FigLabPropskitElement {
|
|
2608
|
+
static observedAttributes = [
|
|
2609
|
+
"options",
|
|
2610
|
+
"value",
|
|
2611
|
+
"default",
|
|
2612
|
+
"aria-label",
|
|
2613
|
+
"disabled",
|
|
2614
|
+
];
|
|
2615
|
+
|
|
2616
|
+
#field = null;
|
|
2617
|
+
#select = null;
|
|
2618
|
+
#optionsPanel = null;
|
|
2586
2619
|
#input = null;
|
|
2587
|
-
#
|
|
2620
|
+
#editButton = null;
|
|
2621
|
+
#addButton = null;
|
|
2622
|
+
#editingValue = "";
|
|
2623
|
+
#initialValue = "";
|
|
2624
|
+
#eventValue = undefined;
|
|
2625
|
+
#reflecting = false;
|
|
2626
|
+
#suppressSelectEvents = false;
|
|
2588
2627
|
#observer = null;
|
|
2589
|
-
#
|
|
2590
|
-
#
|
|
2591
|
-
#
|
|
2592
|
-
#
|
|
2593
|
-
#
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2628
|
+
#menuResizeObserver = null;
|
|
2629
|
+
#menuFrame = 0;
|
|
2630
|
+
#managedSelectAttrs = new Set();
|
|
2631
|
+
#renderedOptionsSignature = null;
|
|
2632
|
+
#boundSelectInput = this.#forwardSelectEvent.bind(this, "input");
|
|
2633
|
+
#boundSelectChange = this.#forwardSelectEvent.bind(this, "change");
|
|
2634
|
+
#boundSelectOptionHover = this.#forwardSelectEvent.bind(
|
|
2635
|
+
this,
|
|
2636
|
+
"optionhover",
|
|
2637
|
+
);
|
|
2638
|
+
#boundEditClick = this.#handleEditClick.bind(this);
|
|
2639
|
+
#boundAddClick = this.#handleAddClick.bind(this);
|
|
2640
|
+
#boundHostClick = this.#handleHostClick.bind(this);
|
|
2641
|
+
#boundDeletePointerDown = this.#handleDeletePointerDown.bind(this);
|
|
2642
|
+
#boundDeleteClick = this.#handleDeleteClick.bind(this);
|
|
2643
|
+
#boundOptionKeydown = this.#handleOptionKeydown.bind(this);
|
|
2644
|
+
#boundPopupToggle = this.#handlePopupToggle.bind(this);
|
|
2645
|
+
#boundEditKeydown = this.#handleEditKeydown.bind(this);
|
|
2646
|
+
#boundEditFocusOut = this.#handleEditFocusOut.bind(this);
|
|
2647
|
+
#boundStopEditEvent = (event) => event.stopImmediatePropagation();
|
|
2598
2648
|
|
|
2599
2649
|
connectedCallback() {
|
|
2600
|
-
if (!this.#
|
|
2601
|
-
this.#
|
|
2602
|
-
this.#
|
|
2603
|
-
this.#bindInputEvents();
|
|
2604
|
-
this.removeEventListener("click", this.#boundHandleClick);
|
|
2605
|
-
this.addEventListener("click", this.#boundHandleClick);
|
|
2650
|
+
if (!this.#field) this.#initialize();
|
|
2651
|
+
this.#syncFromAttributes();
|
|
2652
|
+
this.#bindEvents();
|
|
2606
2653
|
figLabConnectPropskitResetMenu(this);
|
|
2607
2654
|
|
|
2608
2655
|
if (!this.#observer) {
|
|
2609
2656
|
this.#observer = new MutationObserver((mutations) => {
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
syncInput = true;
|
|
2624
|
-
}
|
|
2657
|
+
if (
|
|
2658
|
+
mutations.some(
|
|
2659
|
+
(mutation) =>
|
|
2660
|
+
mutation.type === "attributes" &&
|
|
2661
|
+
mutation.attributeName !== "direction" &&
|
|
2662
|
+
mutation.attributeName !== "aria-disabled" &&
|
|
2663
|
+
!mutation.attributeName?.startsWith("data-") &&
|
|
2664
|
+
!PropskitEditableSelect.observedAttributes.includes(
|
|
2665
|
+
mutation.attributeName,
|
|
2666
|
+
),
|
|
2667
|
+
)
|
|
2668
|
+
) {
|
|
2669
|
+
this.#syncSelectAttributes();
|
|
2625
2670
|
}
|
|
2626
|
-
|
|
2627
|
-
if (syncSurface) this.#syncSurface();
|
|
2628
|
-
if (syncInput) this.#syncInputAttributes();
|
|
2629
2671
|
});
|
|
2630
2672
|
}
|
|
2631
|
-
|
|
2632
2673
|
this.#observer.observe(this, { attributes: true });
|
|
2633
2674
|
}
|
|
2634
2675
|
|
|
2635
2676
|
disconnectedCallback() {
|
|
2636
2677
|
this.#observer?.disconnect();
|
|
2637
|
-
this.#
|
|
2638
|
-
this.removeEventListener("click", this.#boundHandleClick);
|
|
2678
|
+
this.#unbindEvents();
|
|
2639
2679
|
figLabDisconnectPropskitResetMenu(this);
|
|
2640
2680
|
}
|
|
2641
2681
|
|
|
2642
2682
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
2643
|
-
if (oldValue === newValue || !this.#
|
|
2644
|
-
if (name === "
|
|
2645
|
-
|
|
2683
|
+
if (oldValue === newValue || !this.#field) return;
|
|
2684
|
+
if (name === "value" && this.#reflecting) return;
|
|
2685
|
+
if (name === "disabled") {
|
|
2686
|
+
this.#syncDisabled();
|
|
2687
|
+
return;
|
|
2646
2688
|
}
|
|
2689
|
+
this.#syncFromAttributes();
|
|
2647
2690
|
}
|
|
2648
2691
|
|
|
2649
2692
|
#initialize() {
|
|
2650
|
-
this.#initialValue = this.getAttribute("value")
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
|
|
2693
|
+
this.#initialValue = this.#resolveValue(this.getAttribute("value"));
|
|
2694
|
+
const field = figLabCreateElement("div", {
|
|
2695
|
+
className: "propskit-editable-select-surface",
|
|
2696
|
+
});
|
|
2697
|
+
const select = figLabCreateElement("fig-select", {
|
|
2698
|
+
subtle: true,
|
|
2699
|
+
});
|
|
2700
|
+
const optionsPanel = figLabCreateElement("fig-select-options", {
|
|
2701
|
+
className: "propskit-editable-select-options",
|
|
2702
|
+
slot: "panel",
|
|
2703
|
+
});
|
|
2704
|
+
select.append(optionsPanel);
|
|
2705
|
+
const editButton = this.#createActionButton(
|
|
2706
|
+
"propskit-editable-select-edit",
|
|
2707
|
+
"Edit item",
|
|
2708
|
+
"edit",
|
|
2709
|
+
);
|
|
2710
|
+
const addButton = this.#createActionButton(
|
|
2711
|
+
"propskit-editable-select-add",
|
|
2712
|
+
"Add item",
|
|
2713
|
+
"add",
|
|
2714
|
+
);
|
|
2715
|
+
const editTooltip = figLabCreateElement(
|
|
2716
|
+
"fig-tooltip",
|
|
2717
|
+
{
|
|
2718
|
+
className: "propskit-editable-select-edit-tooltip",
|
|
2719
|
+
text: "Edit item",
|
|
2720
|
+
},
|
|
2721
|
+
editButton,
|
|
2654
2722
|
);
|
|
2655
|
-
const
|
|
2656
|
-
|
|
2723
|
+
const addTooltip = figLabCreateElement(
|
|
2724
|
+
"fig-tooltip",
|
|
2725
|
+
{
|
|
2726
|
+
className: "propskit-editable-select-add-tooltip",
|
|
2727
|
+
text: "Add item",
|
|
2728
|
+
},
|
|
2729
|
+
addButton,
|
|
2657
2730
|
);
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2731
|
+
field.append(select, editTooltip, addTooltip);
|
|
2732
|
+
this.#field = field;
|
|
2733
|
+
this.#select = select;
|
|
2734
|
+
this.#optionsPanel = optionsPanel;
|
|
2735
|
+
this.#editButton = editButton;
|
|
2736
|
+
this.#addButton = addButton;
|
|
2737
|
+
this.replaceChildren(field);
|
|
2738
|
+
this.#reflectValue(this.#initialValue);
|
|
2739
|
+
}
|
|
2663
2740
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2741
|
+
#createActionButton(className, label, iconName) {
|
|
2742
|
+
return figLabCreateElement(
|
|
2743
|
+
"fig-button",
|
|
2744
|
+
{
|
|
2745
|
+
className,
|
|
2746
|
+
variant: "secondary",
|
|
2747
|
+
icon: true,
|
|
2748
|
+
"aria-label": label,
|
|
2749
|
+
},
|
|
2750
|
+
figLabCreateElement("fig-icon", {
|
|
2751
|
+
name: iconName,
|
|
2752
|
+
size: "medium",
|
|
2753
|
+
"aria-hidden": "true",
|
|
2754
|
+
}),
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
#parseOptions(value = this.getAttribute("options")) {
|
|
2759
|
+
let parsed = value;
|
|
2760
|
+
if (typeof value === "string") {
|
|
2761
|
+
const text = value.trim();
|
|
2762
|
+
if (text.startsWith("[")) {
|
|
2763
|
+
try {
|
|
2764
|
+
parsed = JSON.parse(text);
|
|
2765
|
+
} catch {
|
|
2766
|
+
parsed = [];
|
|
2767
|
+
}
|
|
2768
|
+
} else {
|
|
2769
|
+
const delimiter = text.includes("\n") ? "\n" : ",";
|
|
2770
|
+
parsed = text
|
|
2771
|
+
.split(delimiter)
|
|
2772
|
+
.map((entry) => entry.trim())
|
|
2773
|
+
.filter(Boolean);
|
|
2774
|
+
}
|
|
2666
2775
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2776
|
+
if (!Array.isArray(parsed)) return [];
|
|
2777
|
+
|
|
2778
|
+
const seen = new Set();
|
|
2779
|
+
const options = [];
|
|
2780
|
+
for (const option of parsed) {
|
|
2781
|
+
const objectOption =
|
|
2782
|
+
option && typeof option === "object" && !Array.isArray(option);
|
|
2783
|
+
const optionValue = objectOption
|
|
2784
|
+
? option.value ?? option.label ?? ""
|
|
2785
|
+
: option;
|
|
2786
|
+
const optionLabel = objectOption
|
|
2787
|
+
? option.label ?? option.value ?? ""
|
|
2788
|
+
: option;
|
|
2789
|
+
const normalizedValue = String(optionValue ?? "").trim();
|
|
2790
|
+
const normalizedLabel = String(optionLabel ?? "").trim();
|
|
2791
|
+
if (!normalizedValue || seen.has(normalizedValue)) continue;
|
|
2792
|
+
seen.add(normalizedValue);
|
|
2793
|
+
options.push({
|
|
2794
|
+
value: normalizedValue,
|
|
2795
|
+
label: normalizedLabel || normalizedValue,
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
return options;
|
|
2673
2799
|
}
|
|
2674
2800
|
|
|
2675
|
-
#
|
|
2676
|
-
|
|
2677
|
-
const
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
);
|
|
2683
|
-
figLabSyncPropskitControlLabel(this, this.#input, labelId, "Text");
|
|
2801
|
+
#resolveValue(value) {
|
|
2802
|
+
const options = this.#parseOptions();
|
|
2803
|
+
const requested = String(value ?? "").trim();
|
|
2804
|
+
if (requested && options.some((option) => option.value === requested)) {
|
|
2805
|
+
return requested;
|
|
2806
|
+
}
|
|
2807
|
+
return options[0]?.value || "";
|
|
2684
2808
|
}
|
|
2685
2809
|
|
|
2686
|
-
#
|
|
2810
|
+
#reflectValue(value) {
|
|
2811
|
+
const resolved = this.#resolveValue(value);
|
|
2812
|
+
const current = this.getAttribute("value");
|
|
2813
|
+
this.#reflecting = true;
|
|
2814
|
+
try {
|
|
2815
|
+
if (resolved) {
|
|
2816
|
+
if (current !== resolved) this.setAttribute("value", resolved);
|
|
2817
|
+
} else if (current !== null) {
|
|
2818
|
+
this.removeAttribute("value");
|
|
2819
|
+
}
|
|
2820
|
+
} finally {
|
|
2821
|
+
this.#reflecting = false;
|
|
2822
|
+
}
|
|
2823
|
+
if (this.#select) this.#select.value = resolved;
|
|
2824
|
+
return resolved;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
#getForwardedSelectAttrNames() {
|
|
2687
2828
|
const reserved = new Set([
|
|
2829
|
+
"options",
|
|
2830
|
+
"value",
|
|
2831
|
+
"default",
|
|
2832
|
+
"disabled",
|
|
2833
|
+
"name",
|
|
2688
2834
|
"label",
|
|
2835
|
+
"aria-label",
|
|
2836
|
+
"aria-disabled",
|
|
2689
2837
|
"direction",
|
|
2690
2838
|
"oninput",
|
|
2691
2839
|
"onchange",
|
|
2840
|
+
"onoptionhover",
|
|
2692
2841
|
"class",
|
|
2693
2842
|
"style",
|
|
2694
2843
|
"id",
|
|
2695
2844
|
"size",
|
|
2696
|
-
"type",
|
|
2697
|
-
"aria-label",
|
|
2698
|
-
"multiline",
|
|
2699
|
-
"autoresize",
|
|
2700
|
-
"resizable",
|
|
2701
|
-
"default",
|
|
2702
2845
|
"variant",
|
|
2846
|
+
"full",
|
|
2847
|
+
"subtle",
|
|
2848
|
+
"data-editing",
|
|
2703
2849
|
]);
|
|
2704
2850
|
return this.getAttributeNames().filter(
|
|
2705
2851
|
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
2706
2852
|
);
|
|
2707
2853
|
}
|
|
2708
2854
|
|
|
2709
|
-
#
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2855
|
+
#syncFromAttributes() {
|
|
2856
|
+
const resolved = this.#reflectValue(this.getAttribute("value"));
|
|
2857
|
+
if (
|
|
2858
|
+
this.#input &&
|
|
2859
|
+
(!this.#editingValue || resolved !== this.#editingValue)
|
|
2860
|
+
) {
|
|
2861
|
+
this.#cancelEditing(false);
|
|
2862
|
+
}
|
|
2863
|
+
this.#syncSelectAttributes();
|
|
2864
|
+
this.#syncDisabled();
|
|
2865
|
+
}
|
|
2714
2866
|
|
|
2715
|
-
|
|
2716
|
-
|
|
2867
|
+
#syncSelectAttributes() {
|
|
2868
|
+
if (!this.#select) return;
|
|
2869
|
+
const selectAttrs = this.#getForwardedSelectAttrNames();
|
|
2870
|
+
const nextManaged = new Set(selectAttrs);
|
|
2871
|
+
for (const name of this.#managedSelectAttrs) {
|
|
2872
|
+
if (!nextManaged.has(name)) this.#select.removeAttribute(name);
|
|
2717
2873
|
}
|
|
2718
|
-
for (const
|
|
2719
|
-
this.#
|
|
2874
|
+
for (const name of selectAttrs) {
|
|
2875
|
+
this.#select.setAttribute(name, this.getAttribute(name) ?? "");
|
|
2720
2876
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2877
|
+
this.#managedSelectAttrs = nextManaged;
|
|
2878
|
+
|
|
2879
|
+
this.#syncOptionElements();
|
|
2880
|
+
const label = this.getAttribute("aria-label")?.trim() || "Select item";
|
|
2881
|
+
this.#select.setAttribute("label", label);
|
|
2882
|
+
this.#select.setAttribute("aria-label", label);
|
|
2883
|
+
this.#select.setAttribute("subtle", "");
|
|
2884
|
+
this.#select.setAttribute("options", JSON.stringify(this.options));
|
|
2885
|
+
this.#select.value = this.#resolveValue(this.getAttribute("value"));
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
#syncOptionElements() {
|
|
2889
|
+
if (!this.#optionsPanel) return;
|
|
2890
|
+
const options = this.options;
|
|
2891
|
+
const signature = JSON.stringify(options);
|
|
2892
|
+
if (signature === this.#renderedOptionsSignature) return;
|
|
2893
|
+
this.#renderedOptionsSignature = signature;
|
|
2894
|
+
|
|
2895
|
+
for (const option of this.#optionsPanel.querySelectorAll(
|
|
2896
|
+
":scope > fig-select-option",
|
|
2897
|
+
)) {
|
|
2898
|
+
option.remove();
|
|
2723
2899
|
}
|
|
2724
|
-
this.#
|
|
2900
|
+
const endButton = this.#optionsPanel.querySelector(
|
|
2901
|
+
":scope > .fig-overflow-end",
|
|
2902
|
+
);
|
|
2903
|
+
for (const entry of options) {
|
|
2904
|
+
const option = figLabCreateElement("fig-select-option", {
|
|
2905
|
+
value: entry.value,
|
|
2906
|
+
label: entry.label,
|
|
2907
|
+
"aria-label": `${entry.label}. Press Delete to remove this item.`,
|
|
2908
|
+
});
|
|
2909
|
+
const label = figLabCreateElement(
|
|
2910
|
+
"span",
|
|
2911
|
+
{ className: "propskit-editable-select-option-label" },
|
|
2912
|
+
entry.label,
|
|
2913
|
+
);
|
|
2914
|
+
const deleteButton = figLabCreateElement(
|
|
2915
|
+
"fig-button",
|
|
2916
|
+
{
|
|
2917
|
+
className: "propskit-editable-select-delete",
|
|
2918
|
+
variant: "ghost",
|
|
2919
|
+
icon: true,
|
|
2920
|
+
"aria-label": `Delete ${entry.label}`,
|
|
2921
|
+
"aria-hidden": "true",
|
|
2922
|
+
"data-value": entry.value,
|
|
2923
|
+
},
|
|
2924
|
+
figLabCreateElement("fig-icon", {
|
|
2925
|
+
name: "trash",
|
|
2926
|
+
size: "small",
|
|
2927
|
+
"aria-hidden": "true",
|
|
2928
|
+
}),
|
|
2929
|
+
);
|
|
2930
|
+
const deleteTooltip = figLabCreateElement(
|
|
2931
|
+
"fig-tooltip",
|
|
2932
|
+
{
|
|
2933
|
+
className: "propskit-editable-select-delete-tooltip",
|
|
2934
|
+
slot: "append",
|
|
2935
|
+
text: `Delete ${entry.label}`,
|
|
2936
|
+
},
|
|
2937
|
+
deleteButton,
|
|
2938
|
+
);
|
|
2939
|
+
option.append(label, deleteTooltip);
|
|
2940
|
+
if (endButton) this.#optionsPanel.insertBefore(option, endButton);
|
|
2941
|
+
else this.#optionsPanel.append(option);
|
|
2942
|
+
const innerButton =
|
|
2943
|
+
deleteButton.button ||
|
|
2944
|
+
deleteButton.shadowRoot?.querySelector("button, [role='button']");
|
|
2945
|
+
if (innerButton instanceof HTMLElement) {
|
|
2946
|
+
innerButton.inert = true;
|
|
2947
|
+
innerButton.style.pointerEvents = "none";
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2725
2951
|
|
|
2726
|
-
|
|
2952
|
+
#syncDisabled() {
|
|
2953
|
+
if (!this.#select || !this.#editButton || !this.#addButton) return;
|
|
2954
|
+
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
2955
|
+
const listLocked = this.options.length <= 1;
|
|
2956
|
+
this.setAttribute("aria-disabled", String(disabled));
|
|
2957
|
+
this.#select.toggleAttribute("disabled", disabled || listLocked);
|
|
2958
|
+
this.#input?.toggleAttribute("disabled", disabled);
|
|
2959
|
+
for (const deleteButton of this.#optionsPanel?.querySelectorAll(
|
|
2960
|
+
".propskit-editable-select-delete",
|
|
2961
|
+
) || []) {
|
|
2962
|
+
deleteButton.toggleAttribute("disabled", disabled || listLocked);
|
|
2963
|
+
}
|
|
2964
|
+
this.#addButton.toggleAttribute("disabled", disabled || Boolean(this.#input));
|
|
2965
|
+
this.#editButton.toggleAttribute(
|
|
2966
|
+
"disabled",
|
|
2967
|
+
disabled || (!this.#input && !this.value),
|
|
2968
|
+
);
|
|
2727
2969
|
}
|
|
2728
2970
|
|
|
2729
|
-
#
|
|
2730
|
-
if (!this.#
|
|
2731
|
-
|
|
2732
|
-
this
|
|
2733
|
-
this.#
|
|
2734
|
-
|
|
2971
|
+
#syncEditButton() {
|
|
2972
|
+
if (!this.#editButton) return;
|
|
2973
|
+
const editing = Boolean(this.#input);
|
|
2974
|
+
this.toggleAttribute("data-editing", editing);
|
|
2975
|
+
this.#editButton.setAttribute(
|
|
2976
|
+
"aria-label",
|
|
2977
|
+
editing ? "Save item" : "Edit item",
|
|
2978
|
+
);
|
|
2979
|
+
this.#editButton
|
|
2980
|
+
.closest("fig-tooltip")
|
|
2981
|
+
?.setAttribute("text", editing ? "Save item" : "Edit item");
|
|
2982
|
+
this.#editButton.setAttribute("variant", editing ? "primary" : "secondary");
|
|
2983
|
+
const icon = this.#editButton.querySelector("fig-icon");
|
|
2984
|
+
icon?.setAttribute("name", editing ? "checkmark" : "edit");
|
|
2985
|
+
this.#syncDisabled();
|
|
2735
2986
|
}
|
|
2736
2987
|
|
|
2737
|
-
#
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
this.#
|
|
2988
|
+
#bindEvents() {
|
|
2989
|
+
this.#unbindEvents();
|
|
2990
|
+
this.#select?.addEventListener("input", this.#boundSelectInput);
|
|
2991
|
+
this.#select?.addEventListener("change", this.#boundSelectChange);
|
|
2992
|
+
this.#select?.addEventListener(
|
|
2993
|
+
"optionhover",
|
|
2994
|
+
this.#boundSelectOptionHover,
|
|
2995
|
+
);
|
|
2996
|
+
this.#editButton?.addEventListener("click", this.#boundEditClick);
|
|
2997
|
+
this.#addButton?.addEventListener("click", this.#boundAddClick);
|
|
2998
|
+
this.#optionsPanel?.addEventListener(
|
|
2999
|
+
"pointerdown",
|
|
3000
|
+
this.#boundDeletePointerDown,
|
|
3001
|
+
);
|
|
3002
|
+
this.#optionsPanel?.addEventListener("click", this.#boundDeleteClick);
|
|
3003
|
+
this.#optionsPanel?.addEventListener("keydown", this.#boundOptionKeydown);
|
|
3004
|
+
this.addEventListener("click", this.#boundHostClick);
|
|
3005
|
+
const popup = this.#getSelectPopup();
|
|
3006
|
+
popup?.addEventListener("toggle", this.#boundPopupToggle);
|
|
3007
|
+
this.#installMenuPositioning();
|
|
3008
|
+
if (!this.#menuResizeObserver && typeof ResizeObserver === "function") {
|
|
3009
|
+
this.#menuResizeObserver = new ResizeObserver(() => {
|
|
3010
|
+
if (this.#select?.open) this.#queueMenuSurfaceSync();
|
|
3011
|
+
});
|
|
2744
3012
|
}
|
|
3013
|
+
if (this.#field) this.#menuResizeObserver?.observe(this.#field);
|
|
2745
3014
|
}
|
|
2746
3015
|
|
|
2747
|
-
#
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
3016
|
+
#unbindEvents() {
|
|
3017
|
+
this.#select?.removeEventListener("input", this.#boundSelectInput);
|
|
3018
|
+
this.#select?.removeEventListener("change", this.#boundSelectChange);
|
|
3019
|
+
this.#select?.removeEventListener(
|
|
3020
|
+
"optionhover",
|
|
3021
|
+
this.#boundSelectOptionHover,
|
|
3022
|
+
);
|
|
3023
|
+
this.#editButton?.removeEventListener("click", this.#boundEditClick);
|
|
3024
|
+
this.#addButton?.removeEventListener("click", this.#boundAddClick);
|
|
3025
|
+
this.#optionsPanel?.removeEventListener(
|
|
3026
|
+
"pointerdown",
|
|
3027
|
+
this.#boundDeletePointerDown,
|
|
3028
|
+
);
|
|
3029
|
+
this.#optionsPanel?.removeEventListener("click", this.#boundDeleteClick);
|
|
3030
|
+
this.#optionsPanel?.removeEventListener(
|
|
3031
|
+
"keydown",
|
|
3032
|
+
this.#boundOptionKeydown,
|
|
3033
|
+
);
|
|
3034
|
+
this.removeEventListener("click", this.#boundHostClick);
|
|
3035
|
+
this.#getSelectPopup()?.removeEventListener(
|
|
3036
|
+
"toggle",
|
|
3037
|
+
this.#boundPopupToggle,
|
|
3038
|
+
);
|
|
3039
|
+
this.#menuResizeObserver?.disconnect();
|
|
3040
|
+
this.#menuResizeObserver = null;
|
|
3041
|
+
cancelAnimationFrame(this.#menuFrame);
|
|
3042
|
+
this.#menuFrame = 0;
|
|
2753
3043
|
}
|
|
2754
3044
|
|
|
2755
|
-
#
|
|
2756
|
-
|
|
3045
|
+
#getSelectPopup() {
|
|
3046
|
+
return this.#select?.shadowRoot?.querySelector('dialog[is="fig-popup"]');
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
#installMenuPositioning() {
|
|
3050
|
+
const popup = this.#getSelectPopup();
|
|
2757
3051
|
if (
|
|
2758
|
-
|
|
2759
|
-
|
|
3052
|
+
!popup ||
|
|
3053
|
+
popup.__propskitEditableSelectPositioning ||
|
|
3054
|
+
typeof popup.positionPopup !== "function"
|
|
2760
3055
|
) {
|
|
2761
3056
|
return;
|
|
2762
3057
|
}
|
|
2763
|
-
|
|
3058
|
+
const positionPopup = popup.positionPopup.bind(popup);
|
|
3059
|
+
popup.positionPopup = (...args) => {
|
|
3060
|
+
const result = positionPopup(...args);
|
|
3061
|
+
this.#queueMenuSurfaceSync();
|
|
3062
|
+
return result;
|
|
3063
|
+
};
|
|
3064
|
+
popup.__propskitEditableSelectPositioning = true;
|
|
2764
3065
|
}
|
|
2765
3066
|
|
|
2766
|
-
|
|
2767
|
-
|
|
3067
|
+
#handlePopupToggle(event) {
|
|
3068
|
+
if (event.newState === "open" || this.#select?.open) {
|
|
3069
|
+
this.#queueMenuSurfaceSync();
|
|
3070
|
+
}
|
|
2768
3071
|
}
|
|
2769
3072
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
3073
|
+
#queueMenuSurfaceSync() {
|
|
3074
|
+
cancelAnimationFrame(this.#menuFrame);
|
|
3075
|
+
this.#menuFrame = requestAnimationFrame(() => {
|
|
3076
|
+
this.#syncMenuToSurface();
|
|
3077
|
+
this.#menuFrame = requestAnimationFrame(() => {
|
|
3078
|
+
this.#menuFrame = 0;
|
|
3079
|
+
this.#syncMenuToSurface();
|
|
3080
|
+
});
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
#syncMenuToSurface() {
|
|
3085
|
+
const popup = this.#getSelectPopup();
|
|
3086
|
+
if (!popup || !this.#field || !this.#select?.open) return;
|
|
3087
|
+
const surfaceRect = this.#field.getBoundingClientRect();
|
|
3088
|
+
if (!surfaceRect.width) return;
|
|
3089
|
+
popup.style.setProperty("box-sizing", "border-box", "important");
|
|
3090
|
+
popup.style.setProperty("left", `${surfaceRect.left}px`, "important");
|
|
3091
|
+
popup.style.setProperty("width", `${surfaceRect.width}px`, "important");
|
|
3092
|
+
popup.style.setProperty("min-width", `${surfaceRect.width}px`, "important");
|
|
3093
|
+
popup.style.setProperty("max-width", `${surfaceRect.width}px`, "important");
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
#forwardSelectEvent(type, event) {
|
|
3097
|
+
if (event.target !== this.#select) return;
|
|
3098
|
+
event.stopImmediatePropagation();
|
|
3099
|
+
if (this.#suppressSelectEvents) return;
|
|
3100
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3101
|
+
const eventValue =
|
|
3102
|
+
type === "optionhover" && event instanceof CustomEvent
|
|
3103
|
+
? String(event.detail ?? "")
|
|
3104
|
+
: this.#reflectValue(this.#select.value);
|
|
3105
|
+
this.#dispatchOptionEvent(type, eventValue);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
#dispatchOptionEvent(type, value = this.value) {
|
|
3109
|
+
const eventValue = String(value ?? "");
|
|
3110
|
+
const label =
|
|
3111
|
+
this.options.find((option) => option.value === eventValue)?.label || "";
|
|
3112
|
+
this.#eventValue = eventValue;
|
|
3113
|
+
try {
|
|
3114
|
+
figLabDispatchPropskitEvent(this, type, eventValue, { label });
|
|
3115
|
+
} finally {
|
|
3116
|
+
this.#eventValue = undefined;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
#handleEditClick(event) {
|
|
3121
|
+
event.preventDefault();
|
|
3122
|
+
event.stopPropagation();
|
|
3123
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3124
|
+
if (this.#input) this.#commitEditing();
|
|
3125
|
+
else this.#startEditing();
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
#handleHostClick(event) {
|
|
3129
|
+
if (
|
|
3130
|
+
this.#input ||
|
|
3131
|
+
!this.#select ||
|
|
3132
|
+
figLabBooleanAttribute(this, "disabled") ||
|
|
3133
|
+
figLabBooleanAttribute(this.#select, "disabled") ||
|
|
3134
|
+
(event.target instanceof Element &&
|
|
3135
|
+
event.target.closest(
|
|
3136
|
+
".propskit-editable-select-edit, .propskit-editable-select-add, fig-menu",
|
|
3137
|
+
))
|
|
3138
|
+
) {
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (event.target instanceof Element && event.target.closest("fig-select")) {
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
event.preventDefault();
|
|
3145
|
+
this.#select.open = true;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
#handleAddClick(event) {
|
|
3149
|
+
event.preventDefault();
|
|
3150
|
+
event.stopPropagation();
|
|
3151
|
+
if (figLabBooleanAttribute(this, "disabled") || this.#input) return;
|
|
3152
|
+
|
|
3153
|
+
const options = this.options;
|
|
3154
|
+
const value = `item-${options.length}`;
|
|
3155
|
+
this.options = [...options, { value, label: "New item" }];
|
|
3156
|
+
this.#reflectValue(value);
|
|
3157
|
+
this.#syncSelectAttributes();
|
|
3158
|
+
for (const type of ["input", "change"]) {
|
|
3159
|
+
this.#dispatchOptionEvent(type);
|
|
3160
|
+
}
|
|
3161
|
+
this.#startEditing();
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
#handleDeletePointerDown(event) {
|
|
3165
|
+
if (
|
|
3166
|
+
!(event.target instanceof Element) ||
|
|
3167
|
+
!event.target.closest(".propskit-editable-select-delete")
|
|
3168
|
+
) {
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
event.preventDefault();
|
|
3172
|
+
event.stopPropagation();
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
#handleDeleteClick(event) {
|
|
3176
|
+
if (!(event.target instanceof Element)) return;
|
|
3177
|
+
const button = event.target.closest(".propskit-editable-select-delete");
|
|
3178
|
+
if (!button || !this.#optionsPanel?.contains(button)) return;
|
|
3179
|
+
event.preventDefault();
|
|
3180
|
+
event.stopPropagation();
|
|
3181
|
+
event.stopImmediatePropagation();
|
|
3182
|
+
if (
|
|
3183
|
+
figLabBooleanAttribute(this, "disabled") ||
|
|
3184
|
+
figLabBooleanAttribute(button, "disabled")
|
|
3185
|
+
) {
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
this.#deleteOption(button.getAttribute("data-value") || "");
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
#handleOptionKeydown(event) {
|
|
3192
|
+
if (
|
|
3193
|
+
(event.key !== "Delete" && event.key !== "Backspace") ||
|
|
3194
|
+
event.altKey ||
|
|
3195
|
+
event.ctrlKey ||
|
|
3196
|
+
event.metaKey ||
|
|
3197
|
+
event.shiftKey ||
|
|
3198
|
+
!(event.target instanceof Element)
|
|
3199
|
+
) {
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
const option = event.target.closest("fig-select-option");
|
|
3203
|
+
if (!option || option.parentElement !== this.#optionsPanel) return;
|
|
3204
|
+
event.preventDefault();
|
|
3205
|
+
event.stopImmediatePropagation();
|
|
3206
|
+
this.#deleteOption(option.getAttribute("value") || "");
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
#deleteOption(value) {
|
|
3210
|
+
if (
|
|
3211
|
+
!this.#select ||
|
|
3212
|
+
!this.#optionsPanel ||
|
|
3213
|
+
figLabBooleanAttribute(this, "disabled")
|
|
3214
|
+
) {
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const options = this.options;
|
|
3218
|
+
if (options.length <= 1) return;
|
|
3219
|
+
const index = options.findIndex((option) => option.value === value);
|
|
3220
|
+
if (index < 0) return;
|
|
3221
|
+
|
|
3222
|
+
const wasOpen = this.#select.open;
|
|
3223
|
+
const selectedValue = this.value;
|
|
3224
|
+
const nextOptions = options.filter((option) => option.value !== value);
|
|
3225
|
+
const nextFocusIndex = Math.min(index, nextOptions.length - 1);
|
|
3226
|
+
const nextValue =
|
|
3227
|
+
selectedValue === value
|
|
3228
|
+
? nextOptions[Math.max(0, nextFocusIndex)]?.value || ""
|
|
3229
|
+
: selectedValue;
|
|
3230
|
+
|
|
3231
|
+
this.#suppressSelectEvents = true;
|
|
3232
|
+
try {
|
|
3233
|
+
if (nextValue) this.#reflectValue(nextValue);
|
|
3234
|
+
this.options = nextOptions;
|
|
3235
|
+
this.#reflectValue(nextValue);
|
|
3236
|
+
this.#syncSelectAttributes();
|
|
3237
|
+
} finally {
|
|
3238
|
+
this.#suppressSelectEvents = false;
|
|
3239
|
+
}
|
|
3240
|
+
for (const type of ["input", "change"]) {
|
|
3241
|
+
this.#dispatchOptionEvent(type);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
queueMicrotask(() => {
|
|
3245
|
+
if (nextOptions.length > 1) {
|
|
3246
|
+
if (wasOpen) this.#select.open = true;
|
|
3247
|
+
const optionElements = [
|
|
3248
|
+
...this.#optionsPanel.querySelectorAll(
|
|
3249
|
+
":scope > fig-select-option",
|
|
3250
|
+
),
|
|
3251
|
+
];
|
|
3252
|
+
optionElements[Math.max(0, nextFocusIndex)]?.focus();
|
|
3253
|
+
} else {
|
|
3254
|
+
this.#select.open = false;
|
|
3255
|
+
requestAnimationFrame(() => this.#addButton?.focus());
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
#startEditing() {
|
|
3261
|
+
if (
|
|
3262
|
+
!this.#field ||
|
|
3263
|
+
!this.value ||
|
|
3264
|
+
figLabBooleanAttribute(this, "disabled")
|
|
3265
|
+
) {
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
const entry = this.options.find((option) => option.value === this.value);
|
|
3269
|
+
if (!entry) return;
|
|
3270
|
+
const input = figLabCreateElement("fig-input-text", {
|
|
3271
|
+
type: "text",
|
|
3272
|
+
full: true,
|
|
3273
|
+
value: entry.label,
|
|
3274
|
+
"aria-label": `Rename ${entry.label}`,
|
|
3275
|
+
});
|
|
3276
|
+
input.addEventListener("input", this.#boundStopEditEvent);
|
|
3277
|
+
input.addEventListener("change", this.#boundStopEditEvent);
|
|
3278
|
+
input.addEventListener("keydown", this.#boundEditKeydown);
|
|
3279
|
+
input.addEventListener("focusout", this.#boundEditFocusOut);
|
|
3280
|
+
this.#editingValue = entry.value;
|
|
3281
|
+
this.#input = input;
|
|
3282
|
+
this.#select.replaceWith(input);
|
|
3283
|
+
this.#syncEditButton();
|
|
3284
|
+
queueMicrotask(() => {
|
|
3285
|
+
input.focus();
|
|
3286
|
+
input.input?.select?.();
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
#handleEditKeydown(event) {
|
|
3291
|
+
if (event.key === "Enter") {
|
|
3292
|
+
event.preventDefault();
|
|
3293
|
+
event.stopImmediatePropagation();
|
|
3294
|
+
this.#commitEditing();
|
|
3295
|
+
} else if (event.key === "Escape") {
|
|
3296
|
+
event.preventDefault();
|
|
3297
|
+
event.stopImmediatePropagation();
|
|
3298
|
+
this.#cancelEditing();
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
#handleEditFocusOut(event) {
|
|
3303
|
+
if (!this.#input) return;
|
|
3304
|
+
const next = event.relatedTarget;
|
|
3305
|
+
if (next instanceof Node && this.#input.contains(next)) return;
|
|
3306
|
+
if (
|
|
3307
|
+
next instanceof Node &&
|
|
3308
|
+
(next === this.#editButton ||
|
|
3309
|
+
this.#editButton?.contains(next) ||
|
|
3310
|
+
next.getRootNode() instanceof ShadowRoot &&
|
|
3311
|
+
next.getRootNode().host === this.#editButton)
|
|
3312
|
+
) {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
this.#commitEditing(false);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
#finishEditing(focus = true) {
|
|
3319
|
+
if (!this.#field || !this.#select) return;
|
|
3320
|
+
const input = this.#input;
|
|
3321
|
+
this.#input = null;
|
|
3322
|
+
this.#editingValue = "";
|
|
3323
|
+
if (input?.parentElement === this.#field) input.replaceWith(this.#select);
|
|
3324
|
+
else if (this.#select.parentElement !== this.#field) {
|
|
3325
|
+
this.#field.prepend(this.#select);
|
|
3326
|
+
}
|
|
3327
|
+
this.#syncSelectAttributes();
|
|
3328
|
+
this.#syncEditButton();
|
|
3329
|
+
if (focus) queueMicrotask(() => this.#select?.focus());
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
#commitEditing(focus = true) {
|
|
3333
|
+
if (!this.#input || !this.#editingValue) return;
|
|
3334
|
+
const editingValue = this.#editingValue;
|
|
3335
|
+
const options = this.options;
|
|
3336
|
+
const index = options.findIndex(
|
|
3337
|
+
(option) => option.value === editingValue,
|
|
3338
|
+
);
|
|
3339
|
+
if (index < 0) {
|
|
3340
|
+
this.#cancelEditing(focus);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
const label = String(this.#input.value ?? "").trim() || options[index].label;
|
|
3344
|
+
const changed = label !== options[index].label;
|
|
3345
|
+
this.#finishEditing(false);
|
|
3346
|
+
if (changed) {
|
|
3347
|
+
options[index] = { ...options[index], label };
|
|
3348
|
+
this.options = options;
|
|
3349
|
+
}
|
|
3350
|
+
this.#reflectValue(editingValue);
|
|
3351
|
+
this.#syncSelectAttributes();
|
|
3352
|
+
if (changed) {
|
|
3353
|
+
for (const type of ["input", "change"]) {
|
|
3354
|
+
this.#dispatchOptionEvent(type);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (focus) queueMicrotask(() => this.#select?.focus());
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
#cancelEditing(focus = true) {
|
|
3361
|
+
this.#finishEditing(focus);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
get options() {
|
|
3365
|
+
return this.#parseOptions();
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
set options(value) {
|
|
3369
|
+
if (value === null || value === undefined) {
|
|
3370
|
+
this.removeAttribute("options");
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
this.setAttribute("options", JSON.stringify(this.#parseOptions(value)));
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
get value() {
|
|
3377
|
+
return (
|
|
3378
|
+
this.#eventValue ??
|
|
3379
|
+
this.#resolveValue(
|
|
3380
|
+
this.getAttribute("value") ?? this.#select?.value ?? "",
|
|
3381
|
+
)
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
set value(value) {
|
|
3386
|
+
this.#reflectValue(value);
|
|
3387
|
+
this.#syncSelectAttributes();
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
get defaultValue() {
|
|
3391
|
+
const requested = this.hasAttribute("default")
|
|
3392
|
+
? this.getAttribute("default")
|
|
3393
|
+
: this.#initialValue;
|
|
3394
|
+
return this.#resolveValue(requested);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
get isDefault() {
|
|
3398
|
+
return figLabPropskitValuesEqual(this.value, this.defaultValue);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
get editing() {
|
|
3402
|
+
return Boolean(this.#input);
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
resetToDefault() {
|
|
3406
|
+
this.#cancelEditing(false);
|
|
3407
|
+
this.value = this.defaultValue;
|
|
3408
|
+
for (const type of ["input", "change"]) {
|
|
3409
|
+
this.#dispatchOptionEvent(type);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
focus(options) {
|
|
3414
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3415
|
+
if (this.#input) this.#input.focus(options);
|
|
3416
|
+
else this.#select?.focus(options);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
figLabDefineElement("propskit-editable-select", PropskitEditableSelect);
|
|
3420
|
+
|
|
3421
|
+
/* PropsKit text surface */
|
|
3422
|
+
class PropskitText extends FigLabPropskitElement {
|
|
3423
|
+
#surface = null;
|
|
3424
|
+
#label = null;
|
|
3425
|
+
#input = null;
|
|
3426
|
+
#hasCustomLabel = false;
|
|
3427
|
+
#observer = null;
|
|
3428
|
+
#managedInputAttrs = new Set();
|
|
3429
|
+
#boundHandleInput = null;
|
|
3430
|
+
#boundHandleChange = null;
|
|
3431
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
3432
|
+
#initialValue = null;
|
|
3433
|
+
|
|
3434
|
+
static get observedAttributes() {
|
|
3435
|
+
return ["label", "aria-label"];
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
connectedCallback() {
|
|
3439
|
+
if (!this.#surface) this.#initialize();
|
|
3440
|
+
this.#syncSurface();
|
|
3441
|
+
this.#syncInputAttributes();
|
|
3442
|
+
this.#bindInputEvents();
|
|
3443
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
3444
|
+
this.addEventListener("click", this.#boundHandleClick);
|
|
3445
|
+
figLabConnectPropskitResetMenu(this);
|
|
3446
|
+
|
|
3447
|
+
if (!this.#observer) {
|
|
3448
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
3449
|
+
let syncSurface = false;
|
|
3450
|
+
let syncInput = false;
|
|
3451
|
+
|
|
3452
|
+
for (const mutation of mutations) {
|
|
3453
|
+
if (mutation.type !== "attributes") continue;
|
|
3454
|
+
if (
|
|
3455
|
+
mutation.attributeName === "label" ||
|
|
3456
|
+
mutation.attributeName === "aria-label"
|
|
3457
|
+
) {
|
|
3458
|
+
syncSurface = true;
|
|
3459
|
+
} else if (mutation.attributeName === "direction") {
|
|
3460
|
+
continue;
|
|
3461
|
+
} else {
|
|
3462
|
+
syncInput = true;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
if (syncSurface) this.#syncSurface();
|
|
3467
|
+
if (syncInput) this.#syncInputAttributes();
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
this.#observer.observe(this, { attributes: true });
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
disconnectedCallback() {
|
|
3475
|
+
this.#observer?.disconnect();
|
|
3476
|
+
this.#unbindInputEvents();
|
|
3477
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
3478
|
+
figLabDisconnectPropskitResetMenu(this);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
3482
|
+
if (oldValue === newValue || !this.#surface) return;
|
|
3483
|
+
if (name === "label" || name === "aria-label") {
|
|
3484
|
+
this.#syncSurface();
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
#initialize() {
|
|
3489
|
+
this.#initialValue = this.getAttribute("value") ?? "";
|
|
3490
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
3491
|
+
(node) =>
|
|
3492
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
3493
|
+
);
|
|
3494
|
+
const customLabel = initialChildren.find(
|
|
3495
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
3496
|
+
);
|
|
3497
|
+
const surface = figLabCreateElement("div", {
|
|
3498
|
+
className: "propskit-text-surface",
|
|
3499
|
+
});
|
|
3500
|
+
const label = customLabel || document.createElement("label");
|
|
3501
|
+
const input = document.createElement("fig-input-text");
|
|
3502
|
+
|
|
3503
|
+
for (const node of initialChildren) {
|
|
3504
|
+
if (node !== customLabel) input.appendChild(node);
|
|
3505
|
+
}
|
|
3506
|
+
surface.append(label, input);
|
|
3507
|
+
this.#surface = surface;
|
|
3508
|
+
this.#label = label;
|
|
3509
|
+
this.#input = input;
|
|
3510
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
3511
|
+
this.replaceChildren(surface);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
#syncSurface() {
|
|
3515
|
+
if (!this.#surface || !this.#label || !this.#input) return;
|
|
3516
|
+
const labelId = figLabSyncPropskitLabel(
|
|
3517
|
+
this,
|
|
3518
|
+
this.#surface,
|
|
3519
|
+
this.#label,
|
|
3520
|
+
this.#hasCustomLabel,
|
|
3521
|
+
);
|
|
3522
|
+
figLabSyncPropskitControlLabel(this, this.#input, labelId, "Text");
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
#getForwardedInputAttrNames() {
|
|
3526
|
+
const reserved = new Set([
|
|
3527
|
+
"label",
|
|
3528
|
+
"direction",
|
|
3529
|
+
"oninput",
|
|
3530
|
+
"onchange",
|
|
3531
|
+
"class",
|
|
3532
|
+
"style",
|
|
3533
|
+
"id",
|
|
3534
|
+
"size",
|
|
3535
|
+
"type",
|
|
3536
|
+
"aria-label",
|
|
3537
|
+
"multiline",
|
|
3538
|
+
"autoresize",
|
|
3539
|
+
"resizable",
|
|
3540
|
+
"default",
|
|
3541
|
+
"variant",
|
|
3542
|
+
]);
|
|
3543
|
+
return this.getAttributeNames().filter(
|
|
3544
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
#syncInputAttributes() {
|
|
3549
|
+
if (!this.#input) return;
|
|
3550
|
+
const inputAttrs = this.#getForwardedInputAttrNames();
|
|
3551
|
+
const defaultEnabledAttrs = ["multiline", "autoresize"];
|
|
3552
|
+
const nextManaged = new Set([...inputAttrs, ...defaultEnabledAttrs, "type"]);
|
|
3553
|
+
|
|
3554
|
+
for (const attrName of this.#managedInputAttrs) {
|
|
3555
|
+
if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
|
|
3556
|
+
}
|
|
3557
|
+
for (const attrName of inputAttrs) {
|
|
3558
|
+
this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
3559
|
+
}
|
|
3560
|
+
for (const attrName of defaultEnabledAttrs) {
|
|
3561
|
+
this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
3562
|
+
}
|
|
3563
|
+
this.#input.setAttribute("type", "text");
|
|
3564
|
+
|
|
3565
|
+
this.#managedInputAttrs = nextManaged;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
#bindInputEvents() {
|
|
3569
|
+
if (!this.#input) return;
|
|
3570
|
+
this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
|
|
3571
|
+
this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
|
|
3572
|
+
this.#input.addEventListener("input", this.#boundHandleInput);
|
|
3573
|
+
this.#input.addEventListener("change", this.#boundHandleChange);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
#unbindInputEvents() {
|
|
3577
|
+
if (!this.#input) return;
|
|
3578
|
+
if (this.#boundHandleInput) {
|
|
3579
|
+
this.#input.removeEventListener("input", this.#boundHandleInput);
|
|
3580
|
+
}
|
|
3581
|
+
if (this.#boundHandleChange) {
|
|
3582
|
+
this.#input.removeEventListener("change", this.#boundHandleChange);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
#forwardInputEvent(type, event) {
|
|
3587
|
+
event.stopImmediatePropagation();
|
|
3588
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3589
|
+
const value = this.#input?.value ?? "";
|
|
3590
|
+
this.setAttribute("value", String(value));
|
|
3591
|
+
figLabDispatchPropskitEvent(this, type);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
#handleClick(event) {
|
|
3595
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
3596
|
+
if (
|
|
3597
|
+
event.target instanceof Element &&
|
|
3598
|
+
event.target.closest("fig-input-text, fig-menu")
|
|
3599
|
+
) {
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
this.focus();
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
get value() {
|
|
3606
|
+
return this.#input?.value ?? this.getAttribute("value") ?? "";
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
set value(nextValue) {
|
|
3610
|
+
if (nextValue === null || nextValue === undefined) {
|
|
3611
|
+
this.removeAttribute("value");
|
|
3612
|
+
if (this.#input) this.#input.value = "";
|
|
3613
|
+
} else {
|
|
3614
|
+
const next = String(nextValue);
|
|
3615
|
+
this.setAttribute("value", next);
|
|
3616
|
+
if (this.#input) this.#input.value = next;
|
|
3617
|
+
}
|
|
2779
3618
|
}
|
|
2780
3619
|
|
|
2781
3620
|
get defaultValue() {
|
|
@@ -4186,6 +5025,455 @@ figLabDefineElement("propskit-easing", PropskitEasing);
|
|
|
4186
5025
|
class PropskitSpring extends PropskitCurve {}
|
|
4187
5026
|
figLabDefineElement("propskit-spring", PropskitSpring);
|
|
4188
5027
|
|
|
5028
|
+
/**
|
|
5029
|
+
* Labeled image chooser with built-in upload and per-image removal actions.
|
|
5030
|
+
*
|
|
5031
|
+
* @attr {string} options - JSON array of image URLs.
|
|
5032
|
+
* @attr {string} value - Selected image URL.
|
|
5033
|
+
* @attr {string} default - Reset image URL.
|
|
5034
|
+
* @attr {string} label - Surface label. Omitted values use "Label"; empty hides it.
|
|
5035
|
+
* @attr {boolean|string} disabled - Disables uploading and image selection.
|
|
5036
|
+
* @fires input - Shared PropsKit event with the selected image URL.
|
|
5037
|
+
* @fires change - Shared PropsKit event with the selected image URL.
|
|
5038
|
+
*/
|
|
5039
|
+
class PropskitImage extends FigLabPropskitElement {
|
|
5040
|
+
static observedAttributes = [
|
|
5041
|
+
"options",
|
|
5042
|
+
"value",
|
|
5043
|
+
"default",
|
|
5044
|
+
"label",
|
|
5045
|
+
"aria-label",
|
|
5046
|
+
"disabled",
|
|
5047
|
+
];
|
|
5048
|
+
|
|
5049
|
+
#surface = null;
|
|
5050
|
+
#header = null;
|
|
5051
|
+
#label = null;
|
|
5052
|
+
#hasCustomLabel = false;
|
|
5053
|
+
#uploadButton = null;
|
|
5054
|
+
#fileInput = null;
|
|
5055
|
+
#chooser = null;
|
|
5056
|
+
#chooserObserver = null;
|
|
5057
|
+
#initialValue = "";
|
|
5058
|
+
#reflecting = false;
|
|
5059
|
+
#blobUrls = new Set();
|
|
5060
|
+
#uploadLabels = new Map();
|
|
5061
|
+
#boundChooserInput = this.#handleChooserEvent.bind(this, "input");
|
|
5062
|
+
#boundChooserChange = this.#handleChooserEvent.bind(this, "change");
|
|
5063
|
+
#boundFileInput = (event) => event.stopImmediatePropagation();
|
|
5064
|
+
#boundFileChange = this.#handleFileChange.bind(this);
|
|
5065
|
+
|
|
5066
|
+
connectedCallback() {
|
|
5067
|
+
if (!this.#surface) {
|
|
5068
|
+
this.#initialValue = this.#resolveValue(this.getAttribute("value"));
|
|
5069
|
+
this.#reflectValue(this.#initialValue);
|
|
5070
|
+
this.#render();
|
|
5071
|
+
}
|
|
5072
|
+
this.#syncLabel();
|
|
5073
|
+
this.#syncChoices();
|
|
5074
|
+
this.#syncDisabled();
|
|
5075
|
+
this.#bindEvents();
|
|
5076
|
+
this.#chooserObserver?.observe(this.#chooser, { childList: true });
|
|
5077
|
+
this.#syncNavigationAccessibility();
|
|
5078
|
+
figLabConnectPropskitResetMenu(this);
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
disconnectedCallback() {
|
|
5082
|
+
this.#unbindEvents();
|
|
5083
|
+
this.#chooserObserver?.disconnect();
|
|
5084
|
+
figLabDisconnectPropskitResetMenu(this);
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5088
|
+
if (oldValue === newValue || !this.#surface) return;
|
|
5089
|
+
if (name === "options") {
|
|
5090
|
+
this.#releaseRemovedBlobUrls();
|
|
5091
|
+
this.#syncChoices();
|
|
5092
|
+
} else if (name === "value" && !this.#reflecting) {
|
|
5093
|
+
this.#reflectValue(newValue);
|
|
5094
|
+
} else if (name === "label" || name === "aria-label") {
|
|
5095
|
+
this.#syncLabel();
|
|
5096
|
+
} else if (name === "disabled") {
|
|
5097
|
+
this.#syncDisabled();
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
#parseOptions(value = this.getAttribute("options")) {
|
|
5102
|
+
let parsed = value;
|
|
5103
|
+
if (typeof value === "string") {
|
|
5104
|
+
try {
|
|
5105
|
+
parsed = JSON.parse(value || "[]");
|
|
5106
|
+
} catch {
|
|
5107
|
+
return [];
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
if (!Array.isArray(parsed)) return [];
|
|
5111
|
+
const unique = new Set();
|
|
5112
|
+
for (const option of parsed) {
|
|
5113
|
+
if (typeof option !== "string") continue;
|
|
5114
|
+
const url = option.trim();
|
|
5115
|
+
if (url) unique.add(url);
|
|
5116
|
+
}
|
|
5117
|
+
return [...unique];
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
#resolveValue(value) {
|
|
5121
|
+
const options = this.#parseOptions();
|
|
5122
|
+
const requested = String(value ?? "").trim();
|
|
5123
|
+
if (requested && options.includes(requested)) return requested;
|
|
5124
|
+
return options[0] || "";
|
|
5125
|
+
}
|
|
5126
|
+
|
|
5127
|
+
#reflectValue(value) {
|
|
5128
|
+
const resolved = this.#resolveValue(value);
|
|
5129
|
+
const current = this.getAttribute("value");
|
|
5130
|
+
if (resolved) {
|
|
5131
|
+
if (current !== resolved) {
|
|
5132
|
+
this.#reflecting = true;
|
|
5133
|
+
this.setAttribute("value", resolved);
|
|
5134
|
+
this.#reflecting = false;
|
|
5135
|
+
}
|
|
5136
|
+
} else if (current !== null) {
|
|
5137
|
+
this.#reflecting = true;
|
|
5138
|
+
this.removeAttribute("value");
|
|
5139
|
+
this.#reflecting = false;
|
|
5140
|
+
}
|
|
5141
|
+
if (this.#chooser) this.#chooser.value = resolved;
|
|
5142
|
+
return resolved;
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
#render() {
|
|
5146
|
+
const customLabel = this.querySelector(":scope > label");
|
|
5147
|
+
const surface = figLabCreateElement("div", {
|
|
5148
|
+
className: "propskit-image-surface",
|
|
5149
|
+
role: "group",
|
|
5150
|
+
});
|
|
5151
|
+
const header = figLabCreateElement("div", {
|
|
5152
|
+
className: "propskit-image-header",
|
|
5153
|
+
});
|
|
5154
|
+
const label = customLabel || document.createElement("label");
|
|
5155
|
+
const uploadTooltip = figLabCreateElement("fig-tooltip", {
|
|
5156
|
+
className: "propskit-image-upload-tooltip",
|
|
5157
|
+
text: "Upload image",
|
|
5158
|
+
});
|
|
5159
|
+
const uploadButton = figLabCreateElement("fig-button", {
|
|
5160
|
+
className: "propskit-image-upload",
|
|
5161
|
+
variant: "ghost",
|
|
5162
|
+
type: "upload",
|
|
5163
|
+
icon: true,
|
|
5164
|
+
"aria-label": "Upload images",
|
|
5165
|
+
});
|
|
5166
|
+
const uploadIcon = figLabCreateElement("fig-icon", {
|
|
5167
|
+
name: "upload",
|
|
5168
|
+
"aria-hidden": "true",
|
|
5169
|
+
});
|
|
5170
|
+
const fileInput = figLabCreateElement("input", {
|
|
5171
|
+
type: "file",
|
|
5172
|
+
accept: "image/*",
|
|
5173
|
+
multiple: true,
|
|
5174
|
+
"aria-label": "Upload images",
|
|
5175
|
+
});
|
|
5176
|
+
const chooser = figLabCreateElement("fig-chooser", {
|
|
5177
|
+
className: "propskit-image-chooser",
|
|
5178
|
+
layout: "grid",
|
|
5179
|
+
columns: "2",
|
|
5180
|
+
overflow: "buttons",
|
|
5181
|
+
full: true,
|
|
5182
|
+
});
|
|
5183
|
+
uploadButton.append(uploadIcon, fileInput);
|
|
5184
|
+
uploadTooltip.append(uploadButton);
|
|
5185
|
+
header.append(label, uploadTooltip);
|
|
5186
|
+
surface.append(header, chooser);
|
|
5187
|
+
this.#surface = surface;
|
|
5188
|
+
this.#header = header;
|
|
5189
|
+
this.#label = label;
|
|
5190
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
5191
|
+
this.#uploadButton = uploadButton;
|
|
5192
|
+
this.#fileInput = fileInput;
|
|
5193
|
+
this.#chooser = chooser;
|
|
5194
|
+
this.#chooserObserver = new MutationObserver(() =>
|
|
5195
|
+
this.#syncNavigationAccessibility(),
|
|
5196
|
+
);
|
|
5197
|
+
this.replaceChildren(surface);
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
#syncLabel() {
|
|
5201
|
+
if (!this.#header || !this.#label || !this.#chooser || !this.#surface) {
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
const labelId = figLabSyncPropskitLabel(
|
|
5205
|
+
this,
|
|
5206
|
+
this.#header,
|
|
5207
|
+
this.#label,
|
|
5208
|
+
this.#hasCustomLabel,
|
|
5209
|
+
);
|
|
5210
|
+
figLabSyncPropskitControlLabel(this, this.#chooser, labelId, "Images");
|
|
5211
|
+
const explicitLabel = this.getAttribute("aria-label")?.trim();
|
|
5212
|
+
if (explicitLabel) {
|
|
5213
|
+
this.#surface.setAttribute("aria-label", explicitLabel);
|
|
5214
|
+
this.#surface.removeAttribute("aria-labelledby");
|
|
5215
|
+
} else if (labelId) {
|
|
5216
|
+
this.#surface.setAttribute("aria-labelledby", labelId);
|
|
5217
|
+
this.#surface.removeAttribute("aria-label");
|
|
5218
|
+
} else {
|
|
5219
|
+
this.#surface.setAttribute("aria-label", "Images");
|
|
5220
|
+
this.#surface.removeAttribute("aria-labelledby");
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
#imageLabel(url, index) {
|
|
5225
|
+
const uploadedLabel = this.#uploadLabels.get(url);
|
|
5226
|
+
if (uploadedLabel) return uploadedLabel;
|
|
5227
|
+
try {
|
|
5228
|
+
const filename = new URL(url, document.baseURI).pathname
|
|
5229
|
+
.split("/")
|
|
5230
|
+
.filter(Boolean)
|
|
5231
|
+
.pop();
|
|
5232
|
+
if (filename) return decodeURIComponent(filename);
|
|
5233
|
+
} catch {}
|
|
5234
|
+
return `Image ${index + 1}`;
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5237
|
+
#syncChoices() {
|
|
5238
|
+
if (!this.#chooser) return;
|
|
5239
|
+
const options = this.#parseOptions();
|
|
5240
|
+
const selected = this.#resolveValue(this.getAttribute("value"));
|
|
5241
|
+
const choices = options.map((url, index) => {
|
|
5242
|
+
const label = this.#imageLabel(url, index);
|
|
5243
|
+
const choice = figLabCreateElement("fig-choice", {
|
|
5244
|
+
value: url,
|
|
5245
|
+
"aria-label": label,
|
|
5246
|
+
selected: url === selected,
|
|
5247
|
+
});
|
|
5248
|
+
const image = figLabCreateElement("fig-image", {
|
|
5249
|
+
src: url,
|
|
5250
|
+
alt: "",
|
|
5251
|
+
full: true,
|
|
5252
|
+
"aspect-ratio": "1 / 1",
|
|
5253
|
+
fit: "cover",
|
|
5254
|
+
});
|
|
5255
|
+
const removeTooltip = figLabCreateElement("fig-tooltip", {
|
|
5256
|
+
className: "propskit-image-remove-tooltip",
|
|
5257
|
+
text: "Remove image",
|
|
5258
|
+
});
|
|
5259
|
+
const removeButton = figLabCreateElement("span", {
|
|
5260
|
+
className: "propskit-image-remove",
|
|
5261
|
+
"aria-hidden": "true",
|
|
5262
|
+
"data-propskit-image-remove": "",
|
|
5263
|
+
});
|
|
5264
|
+
const removeIcon = figLabCreateElement("fig-icon", {
|
|
5265
|
+
name: "close",
|
|
5266
|
+
size: "small",
|
|
5267
|
+
"aria-hidden": "true",
|
|
5268
|
+
});
|
|
5269
|
+
removeButton.append(removeIcon);
|
|
5270
|
+
removeButton.addEventListener("click", (event) =>
|
|
5271
|
+
this.#removeOption(url, index, event),
|
|
5272
|
+
);
|
|
5273
|
+
choice.addEventListener("keydown", (event) => {
|
|
5274
|
+
if (event.key !== "Delete" && event.key !== "Backspace") return;
|
|
5275
|
+
this.#removeOption(url, index, event);
|
|
5276
|
+
});
|
|
5277
|
+
removeTooltip.append(removeButton);
|
|
5278
|
+
choice.dataset.propskitImageLabel = label;
|
|
5279
|
+
choice.append(image, removeTooltip);
|
|
5280
|
+
return choice;
|
|
5281
|
+
});
|
|
5282
|
+
this.#chooser.replaceChildren(...choices);
|
|
5283
|
+
this.#chooser.setAttribute("layout", "grid");
|
|
5284
|
+
this.#chooser.setAttribute("columns", "2");
|
|
5285
|
+
this.#chooser.setAttribute("overflow", "buttons");
|
|
5286
|
+
this.#chooser.toggleAttribute("hidden", choices.length === 0);
|
|
5287
|
+
this.#reflectValue(selected);
|
|
5288
|
+
this.#syncChoiceRemovalState();
|
|
5289
|
+
queueMicrotask(() => this.#syncNavigationAccessibility());
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
#syncChoiceRemovalState() {
|
|
5293
|
+
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
5294
|
+
for (const choice of this.#chooser?.querySelectorAll(
|
|
5295
|
+
":scope > fig-choice",
|
|
5296
|
+
) || []) {
|
|
5297
|
+
if (disabled) {
|
|
5298
|
+
choice.removeAttribute("aria-description");
|
|
5299
|
+
choice.removeAttribute("aria-keyshortcuts");
|
|
5300
|
+
} else {
|
|
5301
|
+
choice.setAttribute(
|
|
5302
|
+
"aria-description",
|
|
5303
|
+
"Press Delete or Backspace to remove this image.",
|
|
5304
|
+
);
|
|
5305
|
+
choice.setAttribute("aria-keyshortcuts", "Delete Backspace");
|
|
5306
|
+
}
|
|
5307
|
+
const removeButton = choice.querySelector(
|
|
5308
|
+
":scope > .propskit-image-remove-tooltip > .propskit-image-remove",
|
|
5309
|
+
);
|
|
5310
|
+
removeButton?.setAttribute("aria-hidden", "true");
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
#syncNavigationAccessibility() {
|
|
5315
|
+
for (const button of this.#chooser?.querySelectorAll(
|
|
5316
|
+
":scope > [data-fig-chooser-nav]",
|
|
5317
|
+
) || []) {
|
|
5318
|
+
button.setAttribute("aria-hidden", "true");
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
#syncDisabled() {
|
|
5323
|
+
if (
|
|
5324
|
+
!this.#surface ||
|
|
5325
|
+
!this.#uploadButton ||
|
|
5326
|
+
!this.#fileInput ||
|
|
5327
|
+
!this.#chooser
|
|
5328
|
+
) {
|
|
5329
|
+
return;
|
|
5330
|
+
}
|
|
5331
|
+
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
5332
|
+
this.#surface.setAttribute("aria-disabled", String(disabled));
|
|
5333
|
+
this.#uploadButton.toggleAttribute("disabled", disabled);
|
|
5334
|
+
this.#fileInput.disabled = disabled;
|
|
5335
|
+
this.#chooser.toggleAttribute("disabled", disabled);
|
|
5336
|
+
this.#chooser.inert = disabled;
|
|
5337
|
+
this.#syncChoiceRemovalState();
|
|
5338
|
+
}
|
|
5339
|
+
|
|
5340
|
+
#bindEvents() {
|
|
5341
|
+
this.#unbindEvents();
|
|
5342
|
+
this.#chooser?.addEventListener("input", this.#boundChooserInput);
|
|
5343
|
+
this.#chooser?.addEventListener("change", this.#boundChooserChange);
|
|
5344
|
+
this.#fileInput?.addEventListener("input", this.#boundFileInput);
|
|
5345
|
+
this.#fileInput?.addEventListener("change", this.#boundFileChange);
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5348
|
+
#unbindEvents() {
|
|
5349
|
+
this.#chooser?.removeEventListener("input", this.#boundChooserInput);
|
|
5350
|
+
this.#chooser?.removeEventListener("change", this.#boundChooserChange);
|
|
5351
|
+
this.#fileInput?.removeEventListener("input", this.#boundFileInput);
|
|
5352
|
+
this.#fileInput?.removeEventListener("change", this.#boundFileChange);
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5355
|
+
#handleChooserEvent(type, event) {
|
|
5356
|
+
if (event.target !== this.#chooser) return;
|
|
5357
|
+
event.stopImmediatePropagation();
|
|
5358
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
5359
|
+
const value = this.#reflectValue(this.#chooser.value);
|
|
5360
|
+
figLabDispatchPropskitEvent(this, type, value);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
#handleFileChange(event) {
|
|
5364
|
+
if (event.target !== this.#fileInput) return;
|
|
5365
|
+
event.stopImmediatePropagation();
|
|
5366
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
5367
|
+
const files = [...(this.#fileInput.files || [])].filter(
|
|
5368
|
+
(file) => !file.type || file.type.startsWith("image/"),
|
|
5369
|
+
);
|
|
5370
|
+
if (!files.length) return;
|
|
5371
|
+
const urls = files.map((file) => {
|
|
5372
|
+
const url = URL.createObjectURL(file);
|
|
5373
|
+
this.#blobUrls.add(url);
|
|
5374
|
+
this.#uploadLabels.set(url, file.name);
|
|
5375
|
+
return url;
|
|
5376
|
+
});
|
|
5377
|
+
this.options = [...this.options, ...urls];
|
|
5378
|
+
this.value = urls[0];
|
|
5379
|
+
this.#fileInput.value = "";
|
|
5380
|
+
for (const type of ["input", "change"]) {
|
|
5381
|
+
figLabDispatchPropskitEvent(this, type, this.value);
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5385
|
+
#removeOption(url, index, event) {
|
|
5386
|
+
event.preventDefault();
|
|
5387
|
+
event.stopImmediatePropagation();
|
|
5388
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
5389
|
+
const options = this.options;
|
|
5390
|
+
if (!options.includes(url)) return;
|
|
5391
|
+
const previousValue = this.value;
|
|
5392
|
+
const removedSelected = previousValue === url;
|
|
5393
|
+
this.options = options.filter((option) => option !== url);
|
|
5394
|
+
const value = this.value;
|
|
5395
|
+
if (value !== previousValue) {
|
|
5396
|
+
for (const type of ["input", "change"]) {
|
|
5397
|
+
figLabDispatchPropskitEvent(this, type, value);
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
queueMicrotask(() => {
|
|
5401
|
+
const choices = [
|
|
5402
|
+
...(this.#chooser?.querySelectorAll(":scope > fig-choice") || []),
|
|
5403
|
+
];
|
|
5404
|
+
const target = removedSelected
|
|
5405
|
+
? this.#chooser?.selectedChoice
|
|
5406
|
+
: choices[Math.min(index, choices.length - 1)];
|
|
5407
|
+
if (target instanceof HTMLElement) {
|
|
5408
|
+
target.focus();
|
|
5409
|
+
} else {
|
|
5410
|
+
this.#fileInput?.focus();
|
|
5411
|
+
}
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
#releaseRemovedBlobUrls() {
|
|
5416
|
+
const options = new Set(this.#parseOptions());
|
|
5417
|
+
for (const url of this.#blobUrls) {
|
|
5418
|
+
if (options.has(url)) continue;
|
|
5419
|
+
URL.revokeObjectURL(url);
|
|
5420
|
+
this.#blobUrls.delete(url);
|
|
5421
|
+
this.#uploadLabels.delete(url);
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
get options() {
|
|
5426
|
+
return this.#parseOptions();
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
set options(value) {
|
|
5430
|
+
if (value === null || value === undefined) {
|
|
5431
|
+
this.removeAttribute("options");
|
|
5432
|
+
return;
|
|
5433
|
+
}
|
|
5434
|
+
this.setAttribute("options", JSON.stringify(this.#parseOptions(value)));
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
get value() {
|
|
5438
|
+
return this.#resolveValue(
|
|
5439
|
+
this.#chooser?.value ?? this.getAttribute("value"),
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
set value(value) {
|
|
5444
|
+
this.#reflectValue(value);
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
get defaultValue() {
|
|
5448
|
+
const fallback = this.#initialValue || this.#resolveValue(null);
|
|
5449
|
+
return this.#resolveValue(
|
|
5450
|
+
this.hasAttribute("default") ? this.getAttribute("default") : fallback,
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
get isDefault() {
|
|
5455
|
+
return figLabPropskitValuesEqual(this.value, this.defaultValue);
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
resetToDefault() {
|
|
5459
|
+
this.value = this.defaultValue;
|
|
5460
|
+
figLabEmitPropskitReset(this);
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
focus(options) {
|
|
5464
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
5465
|
+
const choice =
|
|
5466
|
+
this.#chooser?.selectedChoice ||
|
|
5467
|
+
this.#chooser?.querySelector("fig-choice");
|
|
5468
|
+
if (choice instanceof HTMLElement) {
|
|
5469
|
+
choice.focus(options);
|
|
5470
|
+
} else {
|
|
5471
|
+
this.#fileInput?.focus(options);
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
figLabDefineElement("propskit-image", PropskitImage);
|
|
5476
|
+
|
|
4189
5477
|
/**
|
|
4190
5478
|
* Collapsible color-point group composed from color and position controls.
|
|
4191
5479
|
*
|
|
@@ -5330,6 +6618,7 @@ class PropskitGroup extends FigLabPropskitElement {
|
|
|
5330
6618
|
"propskit-fill",
|
|
5331
6619
|
"propskit-gradient",
|
|
5332
6620
|
"propskit-easing",
|
|
6621
|
+
"propskit-image",
|
|
5333
6622
|
"propskit-joystick",
|
|
5334
6623
|
"propskit-number",
|
|
5335
6624
|
"propskit-origin",
|
|
@@ -5338,6 +6627,7 @@ class PropskitGroup extends FigLabPropskitElement {
|
|
|
5338
6627
|
"propskit-point-radius",
|
|
5339
6628
|
"propskit-point-radius-angle",
|
|
5340
6629
|
"propskit-point-point",
|
|
6630
|
+
"propskit-editable-select",
|
|
5341
6631
|
"propskit-select",
|
|
5342
6632
|
"propskit-slider",
|
|
5343
6633
|
"propskit-spring",
|
|
@@ -11614,13 +12904,16 @@ class FigReorder extends HTMLElement {
|
|
|
11614
12904
|
"fig-input-wheel",
|
|
11615
12905
|
"fig-joystick",
|
|
11616
12906
|
"fig-origin-grid",
|
|
12907
|
+
"fig-chooser",
|
|
11617
12908
|
"fig-canvas-control",
|
|
11618
12909
|
"propskit-color-point",
|
|
12910
|
+
"propskit-image",
|
|
11619
12911
|
"propskit-number",
|
|
11620
12912
|
"propskit-point-point",
|
|
11621
12913
|
"propskit-point-radius",
|
|
11622
12914
|
"propskit-point-radius-angle",
|
|
11623
12915
|
"propskit-position",
|
|
12916
|
+
"propskit-editable-select",
|
|
11624
12917
|
"propskit-slider",
|
|
11625
12918
|
"propskit-oscillator",
|
|
11626
12919
|
];
|