Files
JeyzerMC-comfyui-flakes/web/widgets/flake-data-select.js
T
Mehdi Chaid f0d6205ce7 fix: hide optional field widgets in IntoFlakeDataSelect and improve dropdown style (closes #172)
Auto-created ComfyUI widgets for optional numeric/string fields
(filename_prefix, width, height, steps, cfg, sampler_name, scheduler)
are now hidden on setup — they only appear as input pins when the user
explicitly adds them via the [+] button.

Dropdown appearance updated to use appearance:auto for closer match
to native ComfyUI COMBO widget style.
2026-05-14 18:21:44 -04:00

307 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { css } from "../utils.js";
const ALL_SPLIT_PINS = [
{ name: "model", label: "Model", type: "MODEL" },
{ name: "clip", label: "Clip", type: "CLIP" },
{ name: "vae", label: "VAE", type: "VAE" },
{ name: "positive", label: "Positive", type: "CONDITIONING" },
{ name: "negative", label: "Negative", type: "CONDITIONING" },
{ name: "latent", label: "Latent", type: "LATENT" },
{ name: "filename_prefix", label: "Filename Prefix", type: "STRING" },
{ name: "width", label: "Width", type: "INT" },
{ name: "height", label: "Height", type: "INT" },
{ name: "steps", label: "Steps", type: "INT" },
{ name: "cfg", label: "CFG", type: "FLOAT" },
{ name: "sampler_name", label: "Sampler", type: "STRING" },
{ name: "scheduler", label: "Scheduler", type: "STRING" },
];
const ALL_INTO_PINS = [
{ name: "model", label: "Model", type: "MODEL" },
{ name: "clip", label: "Clip", type: "CLIP" },
{ name: "vae", label: "VAE", type: "VAE" },
{ name: "positive", label: "Positive", type: "CONDITIONING" },
{ name: "negative", label: "Negative", type: "CONDITIONING" },
{ name: "latent", label: "Latent", type: "LATENT" },
{ name: "filename_prefix", label: "Filename Prefix", type: "STRING" },
{ name: "width", label: "Width", type: "INT" },
{ name: "height", label: "Height", type: "INT" },
{ name: "steps", label: "Steps", type: "INT" },
{ name: "cfg", label: "CFG", type: "FLOAT" },
{ name: "sampler_name", label: "Sampler", type: "STRING" },
{ name: "scheduler", label: "Scheduler", type: "STRING" },
];
export const DEFAULT_SPLIT_PINS = ["model"];
export const DEFAULT_INTO_PINS = [];
// ── FlakeDataSplitSelect ──────────────────────────────────────────────────────
// Manages output pin visibility. State lives in node.properties._split_pins.
function syncSplitOutputs(node, selected) {
for (let i = 0; i < node.outputs.length; i++) {
const out = node.outputs[i];
const pinDef = ALL_SPLIT_PINS.find(p => p.name === out.name);
if (!pinDef) continue;
const shouldHide = !selected.includes(out.name);
if (shouldHide && !out.hidden) node.disconnectOutputs(i);
out.hidden = shouldHide;
}
node.setDirtyCanvas(true, true);
const sz = node.computeSize();
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
}
export function setupFlakeDataSplitSelect(node) {
if (!node.properties) node.properties = {};
// Restore saved selection or default
let selected;
try {
const raw = node.properties._split_pins;
selected = Array.isArray(raw) ? [...raw] : [...DEFAULT_SPLIT_PINS];
} catch {
selected = [...DEFAULT_SPLIT_PINS];
}
node.properties._split_pins = [...selected];
syncSplitOutputs(node, selected);
// ── UI ──
const container = document.createElement("div");
css(container, "display:flex;flex-direction:column;gap:4px;padding:4px 6px;font-size:11px;color:#ddd;pointer-events:auto;");
const row = document.createElement("div");
css(row, "display:flex;gap:0;align-items:center;");
const label = document.createElement("span");
css(label, "font-size:10px;opacity:0.7;white-space:nowrap;margin-right:4px;");
label.textContent = "output pin";
row.appendChild(label);
const select = document.createElement("select");
css(select, "flex:1;background:#1a1a1a;color:#ddd;border:1px solid #444;border-right:none;border-radius:4px 0 0 4px;padding:2px 8px 2px 4px;font-size:11px;height:22px;cursor:pointer;outline:none;min-width:0;box-sizing:border-box;appearance:auto;");
for (const pin of ALL_SPLIT_PINS) {
const opt = document.createElement("option");
opt.value = pin.name;
opt.textContent = pin.label;
select.appendChild(opt);
}
row.appendChild(select);
const addBtn = document.createElement("button");
addBtn.textContent = "+";
addBtn.title = "Add output pin";
css(addBtn, "width:22px;height:22px;padding:0;display:flex;align-items:center;justify-content:center;background:#333;color:#ccc;border:1px solid #444;border-radius:0 4px 4px 0;cursor:pointer;font-size:14px;line-height:1;flex-shrink:0;box-sizing:border-box;");
row.appendChild(addBtn);
container.appendChild(row);
const chipRow = document.createElement("div");
css(chipRow, "display:flex;flex-wrap:wrap;gap:3px;min-height:16px;");
container.appendChild(chipRow);
function refresh() {
node.properties._split_pins = [...selected];
syncSplitOutputs(node, selected);
chipRow.replaceChildren();
if (selected.length === 0) {
const empty = document.createElement("span");
css(empty, "font-size:9px;opacity:0.5;");
empty.textContent = "No output pins";
chipRow.appendChild(empty);
} else {
for (const pinName of selected) {
const pinDef = ALL_SPLIT_PINS.find(p => p.name === pinName);
const chip = document.createElement("span");
css(chip, "display:inline-flex;align-items:center;gap:2px;background:#2a3a4a;border:1px solid #3a5a8a;border-radius:3px;padding:1px 5px;font-size:9px;cursor:default;");
chip.textContent = pinDef ? pinDef.label : pinName;
const x = document.createElement("span");
css(x, "color:#8a5a5a;font-weight:bold;font-size:10px;cursor:pointer;margin-left:1px;");
x.textContent = "×";
x.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
selected = selected.filter(n => n !== pinName);
refresh();
});
chip.appendChild(x);
chipRow.appendChild(chip);
}
}
}
addBtn.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
const pinName = select.value;
if (!selected.includes(pinName)) {
selected.push(pinName);
refresh();
}
});
node.addDOMWidget("split_pin_selector", "div", container, { serialize: false, margin: 4 });
node._splitPinUpdate = refresh;
refresh();
const sz = node.computeSize();
node.setSize([Math.max(node.size[0], sz[0], 260), Math.max(node.size[1], sz[1])]);
}
// ── IntoFlakeDataSelect ───────────────────────────────────────────────────────
// Dynamically adds/removes input pins via node.addInput / node.removeInput.
// No default pins. State lives in node.properties._into_pins.
const FIXED_INTO_INPUTS = new Set(["flake_data"]);
function getIntoSelected(node) {
return (node.inputs || [])
.filter(i => !FIXED_INTO_INPUTS.has(i.name))
.map(i => i.name);
}
function addIntoPin(node, pinName) {
const pinDef = ALL_INTO_PINS.find(p => p.name === pinName);
if (!pinDef) return;
// Don't add if already present
if (node.inputs?.find(i => i.name === pinName)) return;
node.addInput(pinName, pinDef.type);
}
function removeIntoPin(node, pinName) {
const idx = node.inputs?.findIndex(i => i.name === pinName);
if (idx == null || idx < 0) return;
if (node.inputs[idx].link != null) node.disconnectInput(idx);
node.removeInput(idx);
}
const OPTIONAL_WIDGET_NAMES = new Set([
"filename_prefix", "width", "height", "steps", "cfg", "sampler_name", "scheduler"
]);
function hideIntoWidgets(node) {
if (!node.widgets) return;
for (const w of node.widgets) {
if (!OPTIONAL_WIDGET_NAMES.has(w.name)) continue;
w.computeSize = () => [0, -4];
w.type = "hidden";
w.hidden = true;
if (w.element) { w.element.style.display = "none"; }
}
}
export function setupIntoFlakeDataSelect(node) {
if (!node.properties) node.properties = {};
// Hide auto-created widgets for optional fields — pins are managed dynamically.
hideIntoWidgets(node);
// Remove any pre-existing optional inputs that Python adds automatically.
// Keep only flake_data.
const toRemove = [];
for (let i = (node.inputs?.length ?? 0) - 1; i >= 0; i--) {
if (!FIXED_INTO_INPUTS.has(node.inputs[i].name)) {
toRemove.push(i);
}
}
for (const idx of toRemove) {
if (node.inputs[idx].link != null) node.disconnectInput(idx);
node.removeInput(idx);
}
// Restore saved pins
const savedPins = node.properties._into_pins;
if (Array.isArray(savedPins)) {
for (const pinName of savedPins) {
addIntoPin(node, pinName);
}
}
// ── UI ──
const container = document.createElement("div");
css(container, "display:flex;flex-direction:column;gap:4px;padding:4px 6px;font-size:11px;color:#ddd;pointer-events:auto;");
const row = document.createElement("div");
css(row, "display:flex;gap:0;align-items:center;");
const label = document.createElement("span");
css(label, "font-size:10px;opacity:0.7;white-space:nowrap;margin-right:4px;");
label.textContent = "input pin";
row.appendChild(label);
// Dropdown styled to match ComfyUI's native COMBO widget (sampler_name style)
const select = document.createElement("select");
css(select, "flex:1;background:#1a1a1a;color:#ddd;border:1px solid #444;border-right:none;border-radius:4px 0 0 4px;padding:2px 8px 2px 4px;font-size:11px;height:22px;cursor:pointer;outline:none;min-width:0;box-sizing:border-box;appearance:auto;");
for (const pin of ALL_INTO_PINS) {
const opt = document.createElement("option");
opt.value = pin.name;
opt.textContent = pin.label;
select.appendChild(opt);
}
row.appendChild(select);
const addBtn = document.createElement("button");
addBtn.textContent = "+";
addBtn.title = "Add input pin";
css(addBtn, "width:22px;height:22px;padding:0;display:flex;align-items:center;justify-content:center;background:#333;color:#ccc;border:1px solid #444;border-radius:0 4px 4px 0;cursor:pointer;font-size:14px;line-height:1;flex-shrink:0;box-sizing:border-box;");
row.appendChild(addBtn);
container.appendChild(row);
const chipRow = document.createElement("div");
css(chipRow, "display:flex;flex-wrap:wrap;gap:3px;min-height:16px;");
container.appendChild(chipRow);
function refresh() {
const current = getIntoSelected(node);
node.properties._into_pins = [...current];
chipRow.replaceChildren();
if (current.length === 0) {
const empty = document.createElement("span");
css(empty, "font-size:9px;opacity:0.5;");
empty.textContent = "No input pins";
chipRow.appendChild(empty);
} else {
for (const pinName of current) {
const pinDef = ALL_INTO_PINS.find(p => p.name === pinName);
const chip = document.createElement("span");
css(chip, "display:inline-flex;align-items:center;gap:2px;background:#2a3a4a;border:1px solid #3a5a8a;border-radius:3px;padding:1px 5px;font-size:9px;cursor:default;");
chip.textContent = pinDef ? pinDef.label : pinName;
const x = document.createElement("span");
css(x, "color:#8a5a5a;font-weight:bold;font-size:10px;cursor:pointer;margin-left:1px;");
x.textContent = "×";
x.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
removeIntoPin(node, pinName);
refresh();
const sz = node.computeSize();
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
});
chip.appendChild(x);
chipRow.appendChild(chip);
}
}
}
addBtn.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
const pinName = select.value;
addIntoPin(node, pinName);
refresh();
const sz = node.computeSize();
node.setSize([Math.max(node.size[0], sz[0]), Math.max(node.size[1], sz[1])]);
});
node.addDOMWidget("into_pin_selector", "div", container, { serialize: false, margin: 4 });
node._intoPinUpdate = refresh;
refresh();
const sz = node.computeSize();
node.setSize([Math.max(node.size[0], sz[0], 260), Math.max(node.size[1], sz[1])]);
}