Files
ausboss-ComfyUI-AusBoss/js/shared/widget_visibility.mjs

135 lines
5.0 KiB
JavaScript

// Show/hide standard canvas widgets in place. Hiding keeps the widget (and
// its serialized value) in the widgets array — only its rendering collapses —
// so widgets_values order never changes and saved workflows stay compatible.
// Pure widget-object surgery, no DOM: testable under node:test.
//
// Two renderers, two switches. The classic canvas lays widgets out by
// computeSize / computeLayoutSize and paints them with draw, so those are
// zeroed. The Nodes 2.0 renderer ignores all of that and filters its widget
// list on `widget.options.hidden` instead, so that flag is set as well - a
// panel's hidden storage widget stays hidden whichever renderer is on.
const STASH = "__ausbossVisibilityStash";
// Collapse a widget to zero height. Returns true when this call changed
// visibility, false when it was already hidden (callers use the return to
// skip a needless node resize).
export function hideWidget(widget) {
if (!widget) return false;
if (widget[STASH]) {
// Already hidden: re-assert the renderer flag, which the frontend
// drops whenever it rebuilds the options object from the node spec.
if (widget.options) widget.options.hidden = true;
return false;
}
let options = widget.options ?? {};
widget[STASH] = {
computeSize: widget.computeSize,
computeLayoutSize: widget.computeLayoutSize,
draw: widget.draw,
hidden: widget.hidden,
optionsHidden: options.hidden,
optionsDescriptor: Object.getOwnPropertyDescriptor(widget, "options"),
};
widget.hidden = true;
options.hidden = true;
// The frontend swaps a widget's options object after creation (the
// after-generate control does, and so does a workflow restore), which
// would drop the flag. An accessor keeps re-applying it to whatever
// object is assigned, so the widget stays hidden under Nodes 2.0.
try {
Object.defineProperty(widget, "options", {
configurable: true,
enumerable: true,
get: () => options,
set: (next) => {
options = next ?? {};
options.hidden = true;
},
});
} catch {
widget.options = options;
}
widget.computeSize = () => [0, -4];
widget.computeLayoutSize = () => ({ minWidth: 0, minHeight: 0 });
widget.draw = () => {};
return true;
}
// Restore a widget hidden by hideWidget. Returns true when this call changed
// visibility, false when the widget was never hidden.
export function showWidget(widget) {
const stash = widget?.[STASH];
if (!stash) return false;
widget.computeSize = stash.computeSize;
widget.computeLayoutSize = stash.computeLayoutSize;
widget.draw = stash.draw;
widget.hidden = stash.hidden;
const options = widget.options;
try {
delete widget.options; // drop the accessor, then put a plain property back
} catch {
// Not configurable: the accessor stays and keeps the flag; nothing to do.
}
if (stash.optionsDescriptor && "value" in stash.optionsDescriptor) {
Object.defineProperty(widget, "options", { ...stash.optionsDescriptor, value: options });
} else {
widget.options = options;
}
if (widget.options) {
if (stash.optionsHidden === undefined) delete widget.options.hidden;
else widget.options.hidden = stash.optionsHidden;
}
widget[STASH] = undefined;
return true;
}
// Drive a widget toward a target visibility; true when anything changed.
export function setWidgetVisible(widget, visible) {
return visible ? showWidget(widget) : hideWidget(widget);
}
// Mark inputs hidden in the node DEFINITION, before any widget exists.
// The frontend rebuilds a widget's options from this spec at several
// points (workflow restore, the after-generate control, the Nodes 2.0
// mount), so a flag written on a live widget is not enough on its own -
// a storage widget a panel always drives is hidden at the source, and
// hideWidget on the live widget covers the classic canvas layout.
//
// Never throws: a renderer that hands out frozen definitions gets a copied
// entry where it allows one and is otherwise left alone - the live-widget
// path still hides the widget on the classic canvas.
export function hideInputsInDef(nodeData, names) {
const groups = nodeData?.input;
if (!groups || typeof groups !== "object") return 0;
let marked = 0;
for (const name of names) {
for (const group of ["required", "optional"]) {
const spec = groups[group]?.[name];
if (!Array.isArray(spec)) continue;
const options = spec[1] && typeof spec[1] === "object" ? spec[1] : {};
if (options.hidden === true) {
marked += 1;
continue;
}
try {
options.hidden = true;
if (spec[1] !== options) spec[1] = options;
if (spec[1]?.hidden === true) {
marked += 1;
continue;
}
} catch {
// frozen options: fall through to replacing the entry
}
try {
groups[group][name] = [spec[0], { ...options, hidden: true }, ...spec.slice(2)];
if (groups[group][name]?.[1]?.hidden === true) marked += 1;
} catch {
// frozen group: nothing more to do here
}
}
}
return marked;
}