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