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

164 lines
5.6 KiB
JavaScript

// Wildcard mode dropdown — shared popup for changing per-wildcard mode.
// Used by Network panel wildcard rows and (future) editor inline badges.
import { app } from "../../../scripts/app.js";
import { closeActivePopup, isPopupOpen, createSearchableList, openPopup, positionPopup } from "./popup-menu.js";
const MODE_EMOJI = {
randomize: "\uD83C\uDFB2", // dice
combine: "\uD83D\uDCDA", // books
iterate: "\u267B\uFE0F", // recycle
none: "\u274C", // cross
};
// Fetch wildcard options from the API (cached per session).
const _optionsCache = new Map();
async function fetchWildcardOptions(name) {
if (_optionsCache.has(name)) return _optionsCache.get(name);
try {
const res = await fetch(`/promptchain/wildcard?name=${encodeURIComponent(name)}&options=true`);
const data = await res.json();
const options = data.options || [];
_optionsCache.set(name, options);
return options;
} catch {
return [];
}
}
// Read the current mode for a wildcard from node properties.
function getWildcardMode(node, name) {
const modes = node.properties?.pcrWildcardModes;
if (!modes || !modes[name]) return { mode: "randomize", index: 0 };
return modes[name];
}
// Write a mode for a wildcard to node properties.
function setWildcardMode(node, name, mode, index, label) {
if (!node.properties) node.properties = {};
if (!node.properties.pcrWildcardModes) node.properties.pcrWildcardModes = {};
// "randomize" is the default — remove the entry entirely to keep properties clean
if (mode === "randomize") {
delete node.properties.pcrWildcardModes[name];
if (Object.keys(node.properties.pcrWildcardModes).length === 0) {
delete node.properties.pcrWildcardModes;
}
} else {
const entry = { mode, index: index || 0 };
if (label) entry.label = label;
node.properties.pcrWildcardModes[name] = entry;
}
app.graph?.setDirtyCanvas?.(true, true);
}
/**
* Show the wildcard mode popup for a given wildcard name.
*
* @param {object} node - The ComfyUI graph node that contains the wildcard.
* @param {string} wildcardName - The wildcard name (e.g. "gkr-wildcards/gkr-cyberpunk/archetype").
* @param {DOMRect|object} triggerRect - Bounding rect of the clicked element.
* @param {function} onChanged - Callback after mode/selection changes.
*/
export function showWildcardModePopup(node, wildcardName, triggerRect, onChanged) {
const popupKey = `wc_${node.id}_${wildcardName}`;
if (isPopupOpen(popupKey)) {
closeActivePopup();
return;
}
const current = getWildcardMode(node, wildcardName);
const currentMode = current.mode;
const currentIndex = current.index;
const menu = document.createElement("div");
menu.className = "pcr-mode-menu";
let close;
// -- mode section --
const modeSection = document.createElement("div");
modeSection.className = "pcr-mode-menu-modes";
function createModeItem(label, mode) {
const item = document.createElement("div");
item.className = "pcr-mode-menu-item pcr-mode-menu-mode-option";
if (currentMode === mode) item.classList.add("pcr-mode-menu-selected");
const text = document.createElement("span");
text.textContent = label;
item.appendChild(text);
if (currentMode === mode) {
const check = document.createElement("span");
check.className = "pcr-mode-menu-check";
check.textContent = "\u2713";
item.appendChild(check);
}
item.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
setWildcardMode(node, wildcardName, mode, 0);
if (close) close();
requestAnimationFrame(() => onChanged?.());
});
return item;
}
modeSection.appendChild(createModeItem(`${MODE_EMOJI.randomize} Randomize`, "randomize"));
modeSection.appendChild(createModeItem(`${MODE_EMOJI.combine} Combine`, "combine"));
modeSection.appendChild(createModeItem(`${MODE_EMOJI.iterate} Iterate`, "iterate"));
modeSection.appendChild(createModeItem(`${MODE_EMOJI.none} None`, "none"));
menu.appendChild(modeSection);
// -- searchable options list (loaded async) --
const loadingEl = document.createElement("div");
loadingEl.className = "pcr-mode-menu-item";
loadingEl.style.cssText = "color:#888;font-style:italic;";
loadingEl.textContent = "Loading options\u2026";
menu.appendChild(loadingEl);
close = openPopup(menu, triggerRect, popupKey);
// fetch options and replace loading indicator with searchable list
fetchWildcardOptions(wildcardName).then((rawOptions) => {
if (!menu.parentNode) return; // popup was closed before fetch completed
loadingEl.remove();
if (rawOptions.length === 0) return;
const options = rawOptions.map((label, idx) => ({
index: idx + 1, // 1-based for switch mode
label: label.replace(/:\d+\.?\d*\)/g, ")").replace(/\s+/g, " ").trim(),
fullLabel: label,
}));
const list = createSearchableList({
options,
onSelect: (opt) => {
setWildcardMode(node, wildcardName, "switch", opt.index, opt.label);
if (close) close();
requestAnimationFrame(() => onChanged?.());
},
currentMode: currentMode === "switch" ? "switch" : "",
currentSwitchIndex: currentIndex,
});
if (list.searchContainer) menu.appendChild(list.searchContainer);
if (list.separator) menu.appendChild(list.separator);
menu.appendChild(list.listContainer);
list.renderList();
// reposition now that the menu has grown with options
requestAnimationFrame(() => {
positionPopup(menu, triggerRect);
if (list.searchInput) list.searchInput.focus();
});
});
}
export { MODE_EMOJI, getWildcardMode, setWildcardMode };