mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-01-11 02:15:17 +00:00
Manage group nodes (#2455)
* wip group manage * prototyping ui * tweaks * wip * wip * more wip * fixes add deletion * Fix tests * fixes * Remove test code * typo * fix crash when link is invalid
This commit is contained in:
parent
56d9496b18
commit
18511dd581
@ -1,6 +1,7 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { mergeIfValid } from "./widgetInputs.js";
|
import { mergeIfValid } from "./widgetInputs.js";
|
||||||
|
import { ManageGroupDialog } from "./groupNodeManage.js";
|
||||||
|
|
||||||
const GROUP = Symbol();
|
const GROUP = Symbol();
|
||||||
|
|
||||||
@ -61,11 +62,7 @@ class GroupNodeBuilder {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
case Workflow.InUse.Registered:
|
case Workflow.InUse.Registered:
|
||||||
if (
|
if (!confirm("A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?")) {
|
||||||
!confirm(
|
|
||||||
"An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -151,6 +148,8 @@ export class GroupNodeConfig {
|
|||||||
this.primitiveDefs = {};
|
this.primitiveDefs = {};
|
||||||
this.widgetToPrimitive = {};
|
this.widgetToPrimitive = {};
|
||||||
this.primitiveToWidget = {};
|
this.primitiveToWidget = {};
|
||||||
|
this.nodeInputs = {};
|
||||||
|
this.outputVisibility = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerType(source = "workflow") {
|
async registerType(source = "workflow") {
|
||||||
@ -158,6 +157,7 @@ export class GroupNodeConfig {
|
|||||||
output: [],
|
output: [],
|
||||||
output_name: [],
|
output_name: [],
|
||||||
output_is_list: [],
|
output_is_list: [],
|
||||||
|
output_is_hidden: [],
|
||||||
name: source + "/" + this.name,
|
name: source + "/" + this.name,
|
||||||
display_name: this.name,
|
display_name: this.name,
|
||||||
category: "group nodes" + ("/" + source),
|
category: "group nodes" + ("/" + source),
|
||||||
@ -277,8 +277,7 @@ export class GroupNodeConfig {
|
|||||||
}
|
}
|
||||||
if (input.widget) {
|
if (input.widget) {
|
||||||
const targetDef = globalDefs[node.type];
|
const targetDef = globalDefs[node.type];
|
||||||
const targetWidget =
|
const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
|
||||||
targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
|
|
||||||
|
|
||||||
const widget = [targetWidget[0], config];
|
const widget = [targetWidget[0], config];
|
||||||
const res = mergeIfValid(
|
const res = mergeIfValid(
|
||||||
@ -330,7 +329,8 @@ export class GroupNodeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getInputConfig(node, inputName, seenInputs, config, extra) {
|
getInputConfig(node, inputName, seenInputs, config, extra) {
|
||||||
let name = node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName;
|
const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName];
|
||||||
|
let name = customConfig?.name ?? node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName;
|
||||||
let key = name;
|
let key = name;
|
||||||
let prefix = "";
|
let prefix = "";
|
||||||
// Special handling for primitive to include the title if it is set rather than just "value"
|
// Special handling for primitive to include the title if it is set rather than just "value"
|
||||||
@ -356,7 +356,7 @@ export class GroupNodeConfig {
|
|||||||
config = [config[0], { ...config[1], ...extra }];
|
config = [config[0], { ...config[1], ...extra }];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name, config };
|
return { name, config, customConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
processWidgetInputs(inputs, node, inputNames, seenInputs) {
|
processWidgetInputs(inputs, node, inputNames, seenInputs) {
|
||||||
@ -366,9 +366,7 @@ export class GroupNodeConfig {
|
|||||||
for (const inputName of inputNames) {
|
for (const inputName of inputNames) {
|
||||||
let widgetType = app.getWidgetType(inputs[inputName], inputName);
|
let widgetType = app.getWidgetType(inputs[inputName], inputName);
|
||||||
if (widgetType) {
|
if (widgetType) {
|
||||||
const convertedIndex = node.inputs?.findIndex(
|
const convertedIndex = node.inputs?.findIndex((inp) => inp.name === inputName && inp.widget?.name === inputName);
|
||||||
(inp) => inp.name === inputName && inp.widget?.name === inputName
|
|
||||||
);
|
|
||||||
if (convertedIndex > -1) {
|
if (convertedIndex > -1) {
|
||||||
// This widget has been converted to a widget
|
// This widget has been converted to a widget
|
||||||
// We need to store this in the correct position so link ids line up
|
// We need to store this in the correct position so link ids line up
|
||||||
@ -424,6 +422,7 @@ export class GroupNodeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
|
processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
|
||||||
|
this.nodeInputs[node.index] = {};
|
||||||
for (let i = 0; i < slots.length; i++) {
|
for (let i = 0; i < slots.length; i++) {
|
||||||
const inputName = slots[i];
|
const inputName = slots[i];
|
||||||
if (linksTo[i]) {
|
if (linksTo[i]) {
|
||||||
@ -432,7 +431,11 @@ export class GroupNodeConfig {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
|
const { name, config, customConfig } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
|
||||||
|
|
||||||
|
this.nodeInputs[node.index][inputName] = name;
|
||||||
|
if(customConfig?.visible === false) continue;
|
||||||
|
|
||||||
this.nodeDef.input.required[name] = config;
|
this.nodeDef.input.required[name] = config;
|
||||||
inputMap[i] = this.inputCount++;
|
inputMap[i] = this.inputCount++;
|
||||||
}
|
}
|
||||||
@ -452,6 +455,7 @@ export class GroupNodeConfig {
|
|||||||
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
|
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
|
||||||
defaultInput: true,
|
defaultInput: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nodeDef.input.required[name] = config;
|
this.nodeDef.input.required[name] = config;
|
||||||
this.newToOldWidgetMap[name] = { node, inputName };
|
this.newToOldWidgetMap[name] = { node, inputName };
|
||||||
|
|
||||||
@ -477,9 +481,7 @@ export class GroupNodeConfig {
|
|||||||
this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
|
this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
|
||||||
|
|
||||||
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
|
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
|
||||||
this.#convertedToProcess.push(() =>
|
this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs));
|
||||||
this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)
|
|
||||||
);
|
|
||||||
|
|
||||||
return inputMapping;
|
return inputMapping;
|
||||||
}
|
}
|
||||||
@ -490,8 +492,12 @@ export class GroupNodeConfig {
|
|||||||
// Add outputs
|
// Add outputs
|
||||||
for (let outputId = 0; outputId < def.output.length; outputId++) {
|
for (let outputId = 0; outputId < def.output.length; outputId++) {
|
||||||
const linksFrom = this.linksFrom[node.index];
|
const linksFrom = this.linksFrom[node.index];
|
||||||
if (linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]) {
|
// If this output is linked internally we flag it to hide
|
||||||
// This output is linked internally so we can skip it
|
const hasLink = linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId];
|
||||||
|
const customConfig = this.nodeData.config?.[node.index]?.output?.[outputId];
|
||||||
|
const visible = customConfig?.visible ?? !hasLink;
|
||||||
|
this.outputVisibility.push(visible);
|
||||||
|
if (!visible) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,11 +506,15 @@ export class GroupNodeConfig {
|
|||||||
this.nodeDef.output.push(def.output[outputId]);
|
this.nodeDef.output.push(def.output[outputId]);
|
||||||
this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
|
this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
|
||||||
|
|
||||||
let label = def.output_name?.[outputId] ?? def.output[outputId];
|
let label = customConfig?.name;
|
||||||
const output = node.outputs.find((o) => o.name === label);
|
if (!label) {
|
||||||
if (output?.label) {
|
label = def.output_name?.[outputId] ?? def.output[outputId];
|
||||||
label = output.label;
|
const output = node.outputs.find((o) => o.name === label);
|
||||||
|
if (output?.label) {
|
||||||
|
label = output.label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = label;
|
let name = label;
|
||||||
if (name in seenOutputs) {
|
if (name in seenOutputs) {
|
||||||
const prefix = `${node.title ?? node.type} `;
|
const prefix = `${node.title ?? node.type} `;
|
||||||
@ -677,6 +687,25 @@ export class GroupNodeHandler {
|
|||||||
return this.innerNodes;
|
return this.innerNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.node.recreate = async () => {
|
||||||
|
const id = this.node.id;
|
||||||
|
const sz = this.node.size;
|
||||||
|
const nodes = this.node.convertToNodes();
|
||||||
|
|
||||||
|
const groupNode = LiteGraph.createNode(this.node.type);
|
||||||
|
groupNode.id = id;
|
||||||
|
|
||||||
|
// Reuse the existing nodes for this instance
|
||||||
|
groupNode.setInnerNodes(nodes);
|
||||||
|
groupNode[GROUP].populateWidgets();
|
||||||
|
app.graph.add(groupNode);
|
||||||
|
groupNode.size = [Math.max(groupNode.size[0], sz[0]), Math.max(groupNode.size[1], sz[1])];
|
||||||
|
|
||||||
|
// Remove all converted nodes and relink them
|
||||||
|
groupNode[GROUP].replaceNodes(nodes);
|
||||||
|
return groupNode;
|
||||||
|
};
|
||||||
|
|
||||||
this.node.convertToNodes = () => {
|
this.node.convertToNodes = () => {
|
||||||
const addInnerNodes = () => {
|
const addInnerNodes = () => {
|
||||||
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
||||||
@ -769,6 +798,7 @@ export class GroupNodeHandler {
|
|||||||
const slot = node.inputs[groupSlotId];
|
const slot = node.inputs[groupSlotId];
|
||||||
if (slot.link == null) continue;
|
if (slot.link == null) continue;
|
||||||
const link = app.graph.links[slot.link];
|
const link = app.graph.links[slot.link];
|
||||||
|
if (!link) continue;
|
||||||
// connect this node output to the input of another node
|
// connect this node output to the input of another node
|
||||||
const originNode = app.graph.getNodeById(link.origin_id);
|
const originNode = app.graph.getNodeById(link.origin_id);
|
||||||
originNode.connect(link.origin_slot, newNode, +innerInputId);
|
originNode.connect(link.origin_slot, newNode, +innerInputId);
|
||||||
@ -806,12 +836,23 @@ export class GroupNodeHandler {
|
|||||||
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
||||||
if (optionIndex === -1) optionIndex = options.length;
|
if (optionIndex === -1) optionIndex = options.length;
|
||||||
else optionIndex++;
|
else optionIndex++;
|
||||||
options.splice(optionIndex, 0, null, {
|
options.splice(
|
||||||
content: "Convert to nodes",
|
optionIndex,
|
||||||
callback: () => {
|
0,
|
||||||
return this.convertToNodes();
|
null,
|
||||||
|
{
|
||||||
|
content: "Convert to nodes",
|
||||||
|
callback: () => {
|
||||||
|
return this.convertToNodes();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
content: "Manage Group Node",
|
||||||
|
callback: () => {
|
||||||
|
new ManageGroupDialog(app).show(this.type);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw custom collapse icon to identity this as a group
|
// Draw custom collapse icon to identity this as a group
|
||||||
@ -865,6 +906,28 @@ export class GroupNodeHandler {
|
|||||||
return onExecutionStart?.apply(this, arguments);
|
return onExecutionStart?.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
const onNodeCreated = this.node.onNodeCreated;
|
||||||
|
this.node.onNodeCreated = function () {
|
||||||
|
const config = self.groupData.nodeData.config;
|
||||||
|
if (config) {
|
||||||
|
for (const n in config) {
|
||||||
|
const inputs = config[n]?.input;
|
||||||
|
for (const w in inputs) {
|
||||||
|
if (inputs[w].visible !== false) continue;
|
||||||
|
const widgetName = self.groupData.oldToNewWidgetMap[n][w];
|
||||||
|
const widget = this.widgets.find((w) => w.name === widgetName);
|
||||||
|
if (widget) {
|
||||||
|
widget.type = "hidden";
|
||||||
|
widget.computeSize = () => [0, -4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return onNodeCreated?.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
function handleEvent(type, getId, getEvent) {
|
function handleEvent(type, getId, getEvent) {
|
||||||
const handler = ({ detail }) => {
|
const handler = ({ detail }) => {
|
||||||
const id = getId(detail);
|
const id = getId(detail);
|
||||||
@ -927,13 +990,15 @@ export class GroupNodeHandler {
|
|||||||
continue;
|
continue;
|
||||||
} else if (innerNode.type === "Reroute") {
|
} else if (innerNode.type === "Reroute") {
|
||||||
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
||||||
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
if (rerouteLinks) {
|
||||||
const node = this.innerNodes[targetNodeId];
|
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
||||||
const input = node.inputs[targetSlot];
|
const node = this.innerNodes[targetNodeId];
|
||||||
if (input.widget) {
|
const input = node.inputs[targetSlot];
|
||||||
const widget = node.widgets?.find((w) => w.name === input.widget.name);
|
if (input.widget) {
|
||||||
if (widget) {
|
const widget = node.widgets?.find((w) => w.name === input.widget.name);
|
||||||
widget.value = newValue;
|
if (widget) {
|
||||||
|
widget.value = newValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -975,7 +1040,7 @@ export class GroupNodeHandler {
|
|||||||
const [, , targetNodeId, targetNodeSlot] = link;
|
const [, , targetNodeId, targetNodeSlot] = link;
|
||||||
const targetNode = this.groupData.nodeData.nodes[targetNodeId];
|
const targetNode = this.groupData.nodeData.nodes[targetNodeId];
|
||||||
const inputs = targetNode.inputs;
|
const inputs = targetNode.inputs;
|
||||||
const targetWidget = inputs?.[targetNodeSlot].widget;
|
const targetWidget = inputs?.[targetNodeSlot]?.widget;
|
||||||
if (!targetWidget) return;
|
if (!targetWidget) return;
|
||||||
|
|
||||||
const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
|
const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
|
||||||
@ -983,13 +1048,12 @@ export class GroupNodeHandler {
|
|||||||
if (v == null) return;
|
if (v == null) return;
|
||||||
|
|
||||||
const widgetName = Object.values(map)[0];
|
const widgetName = Object.values(map)[0];
|
||||||
const widget = this.node.widgets.find(w => w.name === widgetName);
|
const widget = this.node.widgets.find((w) => w.name === widgetName);
|
||||||
if(widget) {
|
if (widget) {
|
||||||
widget.value = v;
|
widget.value = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
populateWidgets() {
|
populateWidgets() {
|
||||||
if (!this.node.widgets) return;
|
if (!this.node.widgets) return;
|
||||||
|
|
||||||
@ -1080,7 +1144,7 @@ export class GroupNodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getGroupData(node) {
|
static getGroupData(node) {
|
||||||
return node.constructor?.nodeData?.[GROUP];
|
return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP];
|
||||||
}
|
}
|
||||||
|
|
||||||
static isGroupNode(node) {
|
static isGroupNode(node) {
|
||||||
@ -1112,7 +1176,7 @@ export class GroupNodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addConvertToGroupOptions() {
|
function addConvertToGroupOptions() {
|
||||||
function addOption(options, index) {
|
function addConvertOption(options, index) {
|
||||||
const selected = Object.values(app.canvas.selected_nodes ?? {});
|
const selected = Object.values(app.canvas.selected_nodes ?? {});
|
||||||
const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
|
const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
|
||||||
options.splice(index + 1, null, {
|
options.splice(index + 1, null, {
|
||||||
@ -1124,12 +1188,25 @@ function addConvertToGroupOptions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addManageOption(options, index) {
|
||||||
|
const groups = app.graph.extra?.groupNodes;
|
||||||
|
const disabled = !groups || !Object.keys(groups).length;
|
||||||
|
options.splice(index + 1, null, {
|
||||||
|
content: `Manage Group Nodes`,
|
||||||
|
disabled,
|
||||||
|
callback: () => {
|
||||||
|
new ManageGroupDialog(app).show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add to canvas
|
// Add to canvas
|
||||||
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||||
const options = getCanvasMenuOptions.apply(this, arguments);
|
const options = getCanvasMenuOptions.apply(this, arguments);
|
||||||
const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
|
const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
|
||||||
addOption(options, index);
|
addConvertOption(options, index);
|
||||||
|
addManageOption(options, index + 1);
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1139,7 +1216,7 @@ function addConvertToGroupOptions() {
|
|||||||
const options = getNodeMenuOptions.apply(this, arguments);
|
const options = getNodeMenuOptions.apply(this, arguments);
|
||||||
if (!GroupNodeHandler.isGroupNode(node)) {
|
if (!GroupNodeHandler.isGroupNode(node)) {
|
||||||
const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
|
const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
|
||||||
addOption(options, index);
|
addConvertOption(options, index);
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
149
web/extensions/core/groupNodeManage.css
Normal file
149
web/extensions/core/groupNodeManage.css
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
.comfy-group-manage {
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--fg-color);
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
border-color: black;
|
||||||
|
margin: 20vh auto;
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-outer {
|
||||||
|
max-height: 60vh;
|
||||||
|
min-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-outer > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--comfy-menu-bg);
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-outer > header select {
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--input-text);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.comfy-group-manage main {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.comfy-group-manage .drag-handle {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list {
|
||||||
|
border-right: 1px solid var(--comfy-menu-bg);
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list ul {
|
||||||
|
margin: 40px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list-items {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list li {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 20px 10px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list li:not(.selected):hover div {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list li.selected {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
.comfy-group-manage-list li span {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node {
|
||||||
|
flex: auto;
|
||||||
|
background: var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node > div {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node header {
|
||||||
|
display: flex;
|
||||||
|
background: var(--bg-color);
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node header a {
|
||||||
|
text-align: center;
|
||||||
|
flex: auto;
|
||||||
|
border-right: 1px solid var(--comfy-menu-bg);
|
||||||
|
border-bottom: 1px solid var(--comfy-menu-bg);
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node header a:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node header a:not(.active):hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node header a.active {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page {
|
||||||
|
display: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page div {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page input {
|
||||||
|
border: none;
|
||||||
|
color: var(--input-text);
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page input[type="text"] {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
.comfy-group-manage-node-page label {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.comfy-group-manage footer {
|
||||||
|
border-top: 1px solid var(--comfy-menu-bg);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.comfy-group-manage footer button {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.comfy-group-manage footer button:first-child {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
422
web/extensions/core/groupNodeManage.js
Normal file
422
web/extensions/core/groupNodeManage.js
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
||||||
|
import { DraggableList } from "../../scripts/ui/draggableList.js";
|
||||||
|
import { addStylesheet } from "../../scripts/utils.js";
|
||||||
|
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js";
|
||||||
|
|
||||||
|
addStylesheet(import.meta.url);
|
||||||
|
|
||||||
|
const ORDER = Symbol();
|
||||||
|
|
||||||
|
function merge(target, source) {
|
||||||
|
if (typeof target === "object" && typeof source === "object") {
|
||||||
|
for (const key in source) {
|
||||||
|
const sv = source[key];
|
||||||
|
if (typeof sv === "object") {
|
||||||
|
let tv = target[key];
|
||||||
|
if (!tv) tv = target[key] = {};
|
||||||
|
merge(tv, source[key]);
|
||||||
|
} else {
|
||||||
|
target[key] = sv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManageGroupDialog extends ComfyDialog {
|
||||||
|
/** @type { Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}> } */
|
||||||
|
tabs = {};
|
||||||
|
/** @type { number | null | undefined } */
|
||||||
|
selectedNodeIndex;
|
||||||
|
/** @type { keyof ManageGroupDialog["tabs"] } */
|
||||||
|
selectedTab = "Inputs";
|
||||||
|
/** @type { string | undefined } */
|
||||||
|
selectedGroup;
|
||||||
|
|
||||||
|
/** @type { Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> } */
|
||||||
|
modifications = {};
|
||||||
|
|
||||||
|
get selectedNodeInnerIndex() {
|
||||||
|
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(app) {
|
||||||
|
super();
|
||||||
|
this.app = app;
|
||||||
|
this.element = $el("dialog.comfy-group-manage", {
|
||||||
|
parent: document.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTab(tab) {
|
||||||
|
this.tabs[this.selectedTab].tab.classList.remove("active");
|
||||||
|
this.tabs[this.selectedTab].page.classList.remove("active");
|
||||||
|
this.tabs[tab].tab.classList.add("active");
|
||||||
|
this.tabs[tab].page.classList.add("active");
|
||||||
|
this.selectedTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeNode(index, force) {
|
||||||
|
if (!force && this.selectedNodeIndex === index) return;
|
||||||
|
|
||||||
|
if (this.selectedNodeIndex != null) {
|
||||||
|
this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
|
||||||
|
}
|
||||||
|
this.nodeItems[index].classList.add("selected");
|
||||||
|
this.selectedNodeIndex = index;
|
||||||
|
|
||||||
|
if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
|
||||||
|
this.changeTab("Widgets");
|
||||||
|
}
|
||||||
|
if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
|
||||||
|
this.changeTab("Outputs");
|
||||||
|
}
|
||||||
|
if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
|
||||||
|
this.changeTab("Inputs");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeTab(this.selectedTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupData() {
|
||||||
|
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
||||||
|
this.groupNodeDef = this.groupNodeType.nodeData;
|
||||||
|
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeGroup(group, reset = true) {
|
||||||
|
this.selectedGroup = group;
|
||||||
|
this.getGroupData();
|
||||||
|
|
||||||
|
const nodes = this.groupData.nodeData.nodes;
|
||||||
|
this.nodeItems = nodes.map((n, i) =>
|
||||||
|
$el(
|
||||||
|
"li.draggable-item",
|
||||||
|
{
|
||||||
|
dataset: {
|
||||||
|
nodeindex: n.index + "",
|
||||||
|
},
|
||||||
|
onclick: () => {
|
||||||
|
this.changeNode(i);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
$el("span.drag-handle"),
|
||||||
|
$el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
textContent: n.title ?? n.type,
|
||||||
|
},
|
||||||
|
n.title
|
||||||
|
? $el("span", {
|
||||||
|
textContent: n.type,
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.innerNodesList.replaceChildren(...this.nodeItems);
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
this.selectedNodeIndex = null;
|
||||||
|
this.changeNode(0);
|
||||||
|
} else {
|
||||||
|
const items = this.draggable.getAllItems();
|
||||||
|
let index = items.findIndex(item => item.classList.contains("selected"));
|
||||||
|
if(index === -1) index = this.selectedNodeIndex;
|
||||||
|
this.changeNode(index, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered = [...nodes];
|
||||||
|
this.draggable?.dispose();
|
||||||
|
this.draggable = new DraggableList(this.innerNodesList, "li");
|
||||||
|
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
|
||||||
|
if (oldPosition === newPosition) return;
|
||||||
|
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
||||||
|
for (let i = 0; i < ordered.length; i++) {
|
||||||
|
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
storeModification({ nodeIndex, section, prop, value }) {
|
||||||
|
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
||||||
|
const nodesMod = (groupMod.nodes ??= {});
|
||||||
|
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {});
|
||||||
|
const typeMod = (nodeMod[section] ??= {});
|
||||||
|
if (typeof value === "object") {
|
||||||
|
const objMod = (typeMod[prop] ??= {});
|
||||||
|
Object.assign(objMod, value);
|
||||||
|
} else {
|
||||||
|
typeMod[prop] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||||
|
if (value === placeholder) value = "";
|
||||||
|
|
||||||
|
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
|
||||||
|
if (mods) {
|
||||||
|
if (mods.name != null) {
|
||||||
|
value = mods.name;
|
||||||
|
}
|
||||||
|
if (mods.visible != null) {
|
||||||
|
checked = mods.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $el("div", [
|
||||||
|
$el("input", {
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
type: "text",
|
||||||
|
onchange: (e) => {
|
||||||
|
this.storeModification({ section, prop, value: { name: e.target.value } });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
$el("label", { textContent: "Visible" }, [
|
||||||
|
$el("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked,
|
||||||
|
disabled: !checkable,
|
||||||
|
onchange: (e) => {
|
||||||
|
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildWidgetsPage() {
|
||||||
|
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
||||||
|
const items = Object.keys(widgets ?? {});
|
||||||
|
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
|
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||||
|
this.widgetsPage.replaceChildren(
|
||||||
|
...items.map((oldName) => {
|
||||||
|
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return !!items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildInputsPage() {
|
||||||
|
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
|
||||||
|
const items = Object.keys(inputs ?? {});
|
||||||
|
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
|
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||||
|
this.inputsPage.replaceChildren(
|
||||||
|
...items
|
||||||
|
.map((oldName) => {
|
||||||
|
let value = inputs[oldName];
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
return !!items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOutputsPage() {
|
||||||
|
const nodes = this.groupData.nodeData.nodes;
|
||||||
|
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
|
||||||
|
const outputs = innerNodeDef?.output ?? [];
|
||||||
|
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
||||||
|
|
||||||
|
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
|
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
||||||
|
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
|
||||||
|
const checkable = node.type !== "PrimitiveNode";
|
||||||
|
this.outputsPage.replaceChildren(
|
||||||
|
...outputs
|
||||||
|
.map((type, slot) => {
|
||||||
|
const groupOutputIndex = groupOutputs?.[slot];
|
||||||
|
const oldName = innerNodeDef.output_name?.[slot] ?? type;
|
||||||
|
let value = config?.[slot]?.name;
|
||||||
|
const visible = config?.[slot]?.visible || groupOutputIndex != null;
|
||||||
|
if (!value || value === oldName) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
return this.getEditElement("output", slot, value, oldName, visible, checkable);
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
return !!outputs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(type) {
|
||||||
|
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
this.innerNodesList = $el("ul.comfy-group-manage-list-items");
|
||||||
|
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
||||||
|
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
||||||
|
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
||||||
|
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
|
||||||
|
|
||||||
|
this.tabs = [
|
||||||
|
["Inputs", this.inputsPage],
|
||||||
|
["Widgets", this.widgetsPage],
|
||||||
|
["Outputs", this.outputsPage],
|
||||||
|
].reduce((p, [name, page]) => {
|
||||||
|
p[name] = {
|
||||||
|
tab: $el("a", {
|
||||||
|
onclick: () => {
|
||||||
|
this.changeTab(name);
|
||||||
|
},
|
||||||
|
textContent: name,
|
||||||
|
}),
|
||||||
|
page,
|
||||||
|
};
|
||||||
|
return p;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const outer = $el("div.comfy-group-manage-outer", [
|
||||||
|
$el("header", [
|
||||||
|
$el("h2", "Group Nodes"),
|
||||||
|
$el(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
onchange: (e) => {
|
||||||
|
this.changeGroup(e.target.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupNodes.map((g) =>
|
||||||
|
$el("option", {
|
||||||
|
textContent: g,
|
||||||
|
selected: "workflow/" + g === type,
|
||||||
|
value: g,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
$el("main", [
|
||||||
|
$el("section.comfy-group-manage-list", this.innerNodesList),
|
||||||
|
$el("section.comfy-group-manage-node", [
|
||||||
|
$el(
|
||||||
|
"header",
|
||||||
|
Object.values(this.tabs).map((t) => t.tab)
|
||||||
|
),
|
||||||
|
pages,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
$el("footer", [
|
||||||
|
$el(
|
||||||
|
"button.comfy-btn",
|
||||||
|
{
|
||||||
|
onclick: (e) => {
|
||||||
|
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
|
||||||
|
if (node) {
|
||||||
|
alert("This group node is in use in the current workflow, please first remove these.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
|
||||||
|
delete app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
|
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
||||||
|
}
|
||||||
|
this.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Delete Group Node"
|
||||||
|
),
|
||||||
|
$el(
|
||||||
|
"button.comfy-btn",
|
||||||
|
{
|
||||||
|
onclick: async () => {
|
||||||
|
let nodesByType;
|
||||||
|
let recreateNodes = [];
|
||||||
|
const types = {};
|
||||||
|
for (const g in this.modifications) {
|
||||||
|
const type = app.graph.extra.groupNodes[g];
|
||||||
|
let config = (type.config ??= {});
|
||||||
|
|
||||||
|
let nodeMods = this.modifications[g]?.nodes;
|
||||||
|
if (nodeMods) {
|
||||||
|
const keys = Object.keys(nodeMods);
|
||||||
|
if (nodeMods[keys[0]][ORDER]) {
|
||||||
|
// If any node is reordered, they will all need sequencing
|
||||||
|
const orderedNodes = [];
|
||||||
|
const orderedMods = {};
|
||||||
|
const orderedConfig = {};
|
||||||
|
|
||||||
|
for (const n of keys) {
|
||||||
|
const order = nodeMods[n][ORDER].order;
|
||||||
|
orderedNodes[order] = type.nodes[+n];
|
||||||
|
orderedMods[order] = nodeMods[n];
|
||||||
|
orderedNodes[order].index = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite links
|
||||||
|
for (const l of type.links) {
|
||||||
|
if (l[0] != null) l[0] = type.nodes[l[0]].index;
|
||||||
|
if (l[2] != null) l[2] = type.nodes[l[2]].index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite externals
|
||||||
|
if (type.external) {
|
||||||
|
for (const ext of type.external) {
|
||||||
|
ext[0] = type.nodes[ext[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite modifications
|
||||||
|
for (const id of keys) {
|
||||||
|
if (config[id]) {
|
||||||
|
orderedConfig[type.nodes[id].index] = config[id];
|
||||||
|
}
|
||||||
|
delete config[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
type.nodes = orderedNodes;
|
||||||
|
nodeMods = orderedMods;
|
||||||
|
type.config = config = orderedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(config, nodeMods);
|
||||||
|
}
|
||||||
|
|
||||||
|
types[g] = type;
|
||||||
|
|
||||||
|
if (!nodesByType) {
|
||||||
|
nodesByType = app.graph._nodes.reduce((p, n) => {
|
||||||
|
p[n.type] ??= [];
|
||||||
|
p[n.type].push(n);
|
||||||
|
return p;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = nodesByType["workflow/" + g];
|
||||||
|
if (nodes) recreateNodes.push(...nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
await GroupNodeConfig.registerFromWorkflow(types, {});
|
||||||
|
|
||||||
|
for (const node of recreateNodes) {
|
||||||
|
node.recreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modifications = {};
|
||||||
|
this.app.graph.setDirtyCanvas(true, true);
|
||||||
|
this.changeGroup(this.selectedGroup, false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Save"
|
||||||
|
),
|
||||||
|
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.element.replaceChildren(outer);
|
||||||
|
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
|
||||||
|
this.element.showModal();
|
||||||
|
|
||||||
|
this.element.addEventListener("close", () => {
|
||||||
|
this.draggable?.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/*": ["./*"]
|
"/*": ["./*"]
|
||||||
}
|
},
|
||||||
|
"lib": ["DOM", "ES2022"]
|
||||||
},
|
},
|
||||||
"include": ["."]
|
"include": ["."]
|
||||||
}
|
}
|
||||||
|
@ -239,7 +239,8 @@ LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
|
|||||||
node.flags?.collapsed ||
|
node.flags?.collapsed ||
|
||||||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||||
widget.computedHeight <= 0 ||
|
widget.computedHeight <= 0 ||
|
||||||
widget.type === "converted-widget";
|
widget.type === "converted-widget"||
|
||||||
|
widget.type === "hidden";
|
||||||
element.hidden = hidden;
|
element.hidden = hidden;
|
||||||
element.style.display = hidden ? "none" : null;
|
element.style.display = hidden ? "none" : null;
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
|
@ -4,6 +4,19 @@ import { ComfySettingsDialog } from "./ui/settings.js";
|
|||||||
|
|
||||||
export const ComfyDialog = _ComfyDialog;
|
export const ComfyDialog = _ComfyDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param { string } tag HTML Element Tag and optional classes e.g. div.class1.class2
|
||||||
|
* @param { string | Element | Element[] | {
|
||||||
|
* parent?: Element,
|
||||||
|
* $?: (el: Element) => void,
|
||||||
|
* dataset?: DOMStringMap,
|
||||||
|
* style?: CSSStyleDeclaration,
|
||||||
|
* for?: string
|
||||||
|
* } | undefined } propsOrChildren
|
||||||
|
* @param { Element[] | undefined } [children]
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function $el(tag, propsOrChildren, children) {
|
export function $el(tag, propsOrChildren, children) {
|
||||||
const split = tag.split(".");
|
const split = tag.split(".");
|
||||||
const element = document.createElement(split.shift());
|
const element = document.createElement(split.shift());
|
||||||
@ -12,6 +25,11 @@ export function $el(tag, propsOrChildren, children) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (propsOrChildren) {
|
if (propsOrChildren) {
|
||||||
|
if (typeof propsOrChildren === "string") {
|
||||||
|
propsOrChildren = { textContent: propsOrChildren };
|
||||||
|
} else if (propsOrChildren instanceof Element) {
|
||||||
|
propsOrChildren = [propsOrChildren];
|
||||||
|
}
|
||||||
if (Array.isArray(propsOrChildren)) {
|
if (Array.isArray(propsOrChildren)) {
|
||||||
element.append(...propsOrChildren);
|
element.append(...propsOrChildren);
|
||||||
} else {
|
} else {
|
||||||
@ -35,7 +53,7 @@ export function $el(tag, propsOrChildren, children) {
|
|||||||
|
|
||||||
Object.assign(element, propsOrChildren);
|
Object.assign(element, propsOrChildren);
|
||||||
if (children) {
|
if (children) {
|
||||||
element.append(...children);
|
element.append(...(children instanceof Array ? children : [children]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
287
web/scripts/ui/draggableList.js
Normal file
287
web/scripts/ui/draggableList.js
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
// @ts-check
|
||||||
|
/*
|
||||||
|
Original implementation:
|
||||||
|
https://github.com/TahaSh/drag-to-reorder
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Taha Shashtari
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $el } from "../ui.js";
|
||||||
|
|
||||||
|
$el("style", {
|
||||||
|
parent: document.head,
|
||||||
|
textContent: `
|
||||||
|
.draggable-item {
|
||||||
|
position: relative;
|
||||||
|
will-change: transform;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.draggable-item.is-idle {
|
||||||
|
transition: 0.25s ease transform;
|
||||||
|
}
|
||||||
|
.draggable-item.is-draggable {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
export class DraggableList extends EventTarget {
|
||||||
|
listContainer;
|
||||||
|
draggableItem;
|
||||||
|
pointerStartX;
|
||||||
|
pointerStartY;
|
||||||
|
scrollYMax;
|
||||||
|
itemsGap = 0;
|
||||||
|
items = [];
|
||||||
|
itemSelector;
|
||||||
|
handleClass = "drag-handle";
|
||||||
|
off = [];
|
||||||
|
offDrag = [];
|
||||||
|
|
||||||
|
constructor(element, itemSelector) {
|
||||||
|
super();
|
||||||
|
this.listContainer = element;
|
||||||
|
this.itemSelector = itemSelector;
|
||||||
|
|
||||||
|
if (!this.listContainer) return;
|
||||||
|
|
||||||
|
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
||||||
|
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
||||||
|
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
||||||
|
this.off.push(this.on(document, "touchend", this.dragEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllItems() {
|
||||||
|
if (!this.items?.length) {
|
||||||
|
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
|
||||||
|
this.items.forEach((element) => {
|
||||||
|
element.classList.add("is-idle");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdleItems() {
|
||||||
|
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
isItemAbove(item) {
|
||||||
|
return item.hasAttribute("data-is-above");
|
||||||
|
}
|
||||||
|
|
||||||
|
isItemToggled(item) {
|
||||||
|
return item.hasAttribute("data-is-toggled");
|
||||||
|
}
|
||||||
|
|
||||||
|
on(source, event, listener, options) {
|
||||||
|
listener = listener.bind(this);
|
||||||
|
source.addEventListener(event, listener, options);
|
||||||
|
return () => source.removeEventListener(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStart(e) {
|
||||||
|
if (e.target.classList.contains(this.handleClass)) {
|
||||||
|
this.draggableItem = e.target.closest(this.itemSelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.draggableItem) return;
|
||||||
|
|
||||||
|
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||||
|
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||||
|
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||||
|
|
||||||
|
this.setItemsGap();
|
||||||
|
this.initDraggableItem();
|
||||||
|
this.initItemsState();
|
||||||
|
|
||||||
|
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
||||||
|
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("dragstart", {
|
||||||
|
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemsGap() {
|
||||||
|
if (this.getIdleItems().length <= 1) {
|
||||||
|
this.itemsGap = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item1 = this.getIdleItems()[0];
|
||||||
|
const item2 = this.getIdleItems()[1];
|
||||||
|
|
||||||
|
const item1Rect = item1.getBoundingClientRect();
|
||||||
|
const item2Rect = item2.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
initItemsState() {
|
||||||
|
this.getIdleItems().forEach((item, i) => {
|
||||||
|
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
||||||
|
item.dataset.isAbove = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initDraggableItem() {
|
||||||
|
this.draggableItem.classList.remove("is-idle");
|
||||||
|
this.draggableItem.classList.add("is-draggable");
|
||||||
|
}
|
||||||
|
|
||||||
|
drag(e) {
|
||||||
|
if (!this.draggableItem) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const clientX = e.clientX || e.touches[0].clientX;
|
||||||
|
const clientY = e.clientY || e.touches[0].clientY;
|
||||||
|
|
||||||
|
const listRect = this.listContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (clientY > listRect.bottom) {
|
||||||
|
if (this.listContainer.scrollTop < this.scrollYMax) {
|
||||||
|
this.listContainer.scrollBy(0, 10);
|
||||||
|
this.pointerStartY -= 10;
|
||||||
|
}
|
||||||
|
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
||||||
|
this.pointerStartY += 10;
|
||||||
|
this.listContainer.scrollBy(0, -10);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointerOffsetX = clientX - this.pointerStartX;
|
||||||
|
const pointerOffsetY = clientY - this.pointerStartY;
|
||||||
|
|
||||||
|
this.updateIdleItemsStateAndPosition();
|
||||||
|
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIdleItemsStateAndPosition() {
|
||||||
|
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
||||||
|
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
this.getIdleItems().forEach((item) => {
|
||||||
|
const itemRect = item.getBoundingClientRect();
|
||||||
|
const itemY = itemRect.top + itemRect.height / 2;
|
||||||
|
if (this.isItemAbove(item)) {
|
||||||
|
if (draggableItemY <= itemY) {
|
||||||
|
item.dataset.isToggled = "";
|
||||||
|
} else {
|
||||||
|
delete item.dataset.isToggled;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (draggableItemY >= itemY) {
|
||||||
|
item.dataset.isToggled = "";
|
||||||
|
} else {
|
||||||
|
delete item.dataset.isToggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
this.getIdleItems().forEach((item) => {
|
||||||
|
if (this.isItemToggled(item)) {
|
||||||
|
const direction = this.isItemAbove(item) ? 1 : -1;
|
||||||
|
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
||||||
|
} else {
|
||||||
|
item.style.transform = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dragEnd() {
|
||||||
|
if (!this.draggableItem) return;
|
||||||
|
|
||||||
|
this.applyNewItemsOrder();
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyNewItemsOrder() {
|
||||||
|
const reorderedItems = [];
|
||||||
|
|
||||||
|
let oldPosition = -1;
|
||||||
|
this.getAllItems().forEach((item, index) => {
|
||||||
|
if (item === this.draggableItem) {
|
||||||
|
oldPosition = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.isItemToggled(item)) {
|
||||||
|
reorderedItems[index] = item;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
||||||
|
reorderedItems[newIndex] = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||||
|
const item = reorderedItems[index];
|
||||||
|
if (typeof item === "undefined") {
|
||||||
|
reorderedItems[index] = this.draggableItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reorderedItems.forEach((item) => {
|
||||||
|
this.listContainer.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items = reorderedItems;
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("dragend", {
|
||||||
|
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.itemsGap = 0;
|
||||||
|
this.items = [];
|
||||||
|
this.unsetDraggableItem();
|
||||||
|
this.unsetItemState();
|
||||||
|
|
||||||
|
this.offDrag.forEach((f) => f());
|
||||||
|
this.offDrag = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
unsetDraggableItem() {
|
||||||
|
this.draggableItem.style = null;
|
||||||
|
this.draggableItem.classList.remove("is-draggable");
|
||||||
|
this.draggableItem.classList.add("is-idle");
|
||||||
|
this.draggableItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsetItemState() {
|
||||||
|
this.getIdleItems().forEach((item, i) => {
|
||||||
|
delete item.dataset.isAbove;
|
||||||
|
delete item.dataset.isToggled;
|
||||||
|
item.style.transform = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.off.forEach((f) => f());
|
||||||
|
}
|
||||||
|
}
|
@ -145,6 +145,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comfy-menu span.drag-handle {
|
.comfy-menu span.drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.drag-handle {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -160,12 +166,9 @@ body {
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
color: var(--drag-text);
|
color: var(--drag-text);
|
||||||
text-shadow: 1px 0 1px black;
|
text-shadow: 1px 0 1px black;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comfy-menu span.drag-handle::after {
|
span.drag-handle::after {
|
||||||
content: '.. .. ..';
|
content: '.. .. ..';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user