Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

162 lines
4.7 KiB
Svelte

<script module>
const optionsCache = new Map();
</script>
<script>
// WildcardDropdown — per-wildcard mode selector with async options list.
// Renders directly at mount point (no PopupAnchor portal) since this component
// is standalone-mounted on document.body by the bridge.
import { onMount, onDestroy } from "svelte";
import SearchableList from "./SearchableList.svelte";
let {
wildcardName,
currentMode = "randomize",
currentIndex = 0,
triggerRect,
popupKey = "",
onSelectMode = () => {},
onSelectOption = () => {},
onClose = () => {},
} = $props();
let options = $state([]);
let loading = $state(true);
let loadError = $state(null);
let searchList = $state(null);
let menuEl;
let openedAt = 0;
let fetchAc = null;
const modes = [
{ emoji: "\u{1F3B2}", label: "Randomize", value: "randomize" },
{ emoji: "\u{1F4DA}", label: "Combine", value: "combine" },
{ emoji: "\u267B\uFE0F", label: "Iterate", value: "iterate" },
{ emoji: "\u274C", label: "None", value: "none" },
];
function selectMode(e, mode) {
e.stopPropagation();
onSelectMode(mode);
requestAnimationFrame(() => onClose());
}
function selectOption(opt) {
onSelectOption(opt);
requestAnimationFrame(() => onClose());
}
function dismiss(e) {
if (Date.now() - openedAt < 300) return;
if (menuEl?.contains(e.target)) return;
onClose();
}
function reposition() {
if (!menuEl || !triggerRect) return;
requestAnimationFrame(() => {
if (!menuEl) return;
const rect = menuEl.getBoundingClientRect();
let left = triggerRect.left;
let top = triggerRect.bottom + 4;
if (left + rect.width > window.innerWidth) left = window.innerWidth - rect.width - 10;
if (top + rect.height > window.innerHeight) top = triggerRect.top - rect.height - 4;
if (left < 10) left = 10;
if (top < 10) top = 10;
menuEl.style.left = `${left}px`;
menuEl.style.top = `${top}px`;
});
}
onMount(() => {
openedAt = Date.now();
document.addEventListener("click", dismiss);
document.addEventListener("pointerdown", dismiss);
reposition();
const cached = optionsCache.get(wildcardName);
if (cached) {
options = cached;
loading = false;
requestAnimationFrame(() => { reposition(); searchList?.focusSearch(); });
return;
}
fetchAc = new AbortController();
fetch(`/promptchain/wildcard?name=${encodeURIComponent(wildcardName)}&options=true`,
{ signal: fetchAc.signal })
.then(async r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
const raw = data.options || [];
const mapped = raw.map((label, idx) => ({
index: idx + 1,
label: label.replace(/:\d+\.?\d*\)/g, ")").replace(/\s+/g, " ").trim(),
fullLabel: label,
}));
optionsCache.set(wildcardName, mapped);
options = mapped;
loading = false;
requestAnimationFrame(() => { reposition(); searchList?.focusSearch(); });
})
.catch(e => {
if (e.name === "AbortError") return;
console.error("[PromptChain] wildcard options load failed:", e);
loadError = e.message || "Load failed";
loading = false;
});
});
onDestroy(() => {
fetchAc?.abort();
document.removeEventListener("click", dismiss);
document.removeEventListener("pointerdown", dismiss);
});
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={menuEl}
class="pcr-mode-menu"
style="position:fixed;z-index:100000;"
onclick={(e) => e.stopPropagation()}
onpointerdown={(e) => e.stopPropagation()}
>
<div class="pcr-mode-menu-modes">
{#each modes as mode}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="pcr-mode-menu-item pcr-mode-menu-mode-option"
class:pcr-mode-menu-selected={currentMode === mode.value}
onclick={(e) => selectMode(e, mode.value)}
>
<span>{mode.emoji} {mode.label}</span>
{#if currentMode === mode.value}
<span class="pcr-mode-menu-check">{"\u2713"}</span>
{/if}
</div>
{/each}
</div>
{#if loading}
<div class="pcr-mode-menu-item" style="color:#888;font-style:italic;">
Loading options{"\u2026"}
</div>
{:else if loadError}
<div class="pcr-mode-menu-item" style="color:#e55;font-style:italic;">
Error: {loadError}
</div>
{:else}
<SearchableList
bind:this={searchList}
{options}
onSelect={selectOption}
currentMode={currentMode === "switch" ? "switch" : ""}
currentSwitchIndex={currentIndex}
/>
{/if}
</div>