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 { api } from "../../scripts/api.js";
|
||||
import { mergeIfValid } from "./widgetInputs.js";
|
||||
import { ManageGroupDialog } from "./groupNodeManage.js";
|
||||
|
||||
const GROUP = Symbol();
|
||||
|
||||
@ -61,11 +62,7 @@ class GroupNodeBuilder {
|
||||
);
|
||||
return;
|
||||
case Workflow.InUse.Registered:
|
||||
if (
|
||||
!confirm(
|
||||
"An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?"
|
||||
)
|
||||
) {
|
||||
if (!confirm("A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?")) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@ -151,6 +148,8 @@ export class GroupNodeConfig {
|
||||
this.primitiveDefs = {};
|
||||
this.widgetToPrimitive = {};
|
||||
this.primitiveToWidget = {};
|
||||
this.nodeInputs = {};
|
||||
this.outputVisibility = [];
|
||||
}
|
||||
|
||||
async registerType(source = "workflow") {
|
||||
@ -158,6 +157,7 @@ export class GroupNodeConfig {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
output_is_hidden: [],
|
||||
name: source + "/" + this.name,
|
||||
display_name: this.name,
|
||||
category: "group nodes" + ("/" + source),
|
||||
@ -277,8 +277,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
if (input.widget) {
|
||||
const targetDef = globalDefs[node.type];
|
||||
const targetWidget =
|
||||
targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
|
||||
const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
|
||||
|
||||
const widget = [targetWidget[0], config];
|
||||
const res = mergeIfValid(
|
||||
@ -330,7 +329,8 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
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 prefix = "";
|
||||
// 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 }];
|
||||
}
|
||||
|
||||
return { name, config };
|
||||
return { name, config, customConfig };
|
||||
}
|
||||
|
||||
processWidgetInputs(inputs, node, inputNames, seenInputs) {
|
||||
@ -366,9 +366,7 @@ export class GroupNodeConfig {
|
||||
for (const inputName of inputNames) {
|
||||
let widgetType = app.getWidgetType(inputs[inputName], inputName);
|
||||
if (widgetType) {
|
||||
const convertedIndex = node.inputs?.findIndex(
|
||||
(inp) => inp.name === inputName && inp.widget?.name === inputName
|
||||
);
|
||||
const convertedIndex = node.inputs?.findIndex((inp) => inp.name === inputName && inp.widget?.name === inputName);
|
||||
if (convertedIndex > -1) {
|
||||
// This widget has been converted to a widget
|
||||
// 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) {
|
||||
this.nodeInputs[node.index] = {};
|
||||
for (let i = 0; i < slots.length; i++) {
|
||||
const inputName = slots[i];
|
||||
if (linksTo[i]) {
|
||||
@ -432,7 +431,11 @@ export class GroupNodeConfig {
|
||||
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;
|
||||
inputMap[i] = this.inputCount++;
|
||||
}
|
||||
@ -452,6 +455,7 @@ export class GroupNodeConfig {
|
||||
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
|
||||
defaultInput: true,
|
||||
});
|
||||
|
||||
this.nodeDef.input.required[name] = config;
|
||||
this.newToOldWidgetMap[name] = { node, inputName };
|
||||
|
||||
@ -477,9 +481,7 @@ export class GroupNodeConfig {
|
||||
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
|
||||
this.#convertedToProcess.push(() =>
|
||||
this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)
|
||||
);
|
||||
this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs));
|
||||
|
||||
return inputMapping;
|
||||
}
|
||||
@ -490,8 +492,12 @@ export class GroupNodeConfig {
|
||||
// Add outputs
|
||||
for (let outputId = 0; outputId < def.output.length; outputId++) {
|
||||
const linksFrom = this.linksFrom[node.index];
|
||||
if (linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]) {
|
||||
// This output is linked internally so we can skip it
|
||||
// If this output is linked internally we flag it to hide
|
||||
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;
|
||||
}
|
||||
|
||||
@ -500,11 +506,15 @@ export class GroupNodeConfig {
|
||||
this.nodeDef.output.push(def.output[outputId]);
|
||||
this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
|
||||
|
||||
let label = def.output_name?.[outputId] ?? def.output[outputId];
|
||||
const output = node.outputs.find((o) => o.name === label);
|
||||
if (output?.label) {
|
||||
label = output.label;
|
||||
let label = customConfig?.name;
|
||||
if (!label) {
|
||||
label = def.output_name?.[outputId] ?? def.output[outputId];
|
||||
const output = node.outputs.find((o) => o.name === label);
|
||||
if (output?.label) {
|
||||
label = output.label;
|
||||
}
|
||||
}
|
||||
|
||||
let name = label;
|
||||
if (name in seenOutputs) {
|
||||
const prefix = `${node.title ?? node.type} `;
|
||||
@ -677,6 +687,25 @@ export class GroupNodeHandler {
|
||||
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 = () => {
|
||||
const addInnerNodes = () => {
|
||||
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
||||
@ -769,6 +798,7 @@ export class GroupNodeHandler {
|
||||
const slot = node.inputs[groupSlotId];
|
||||
if (slot.link == null) continue;
|
||||
const link = app.graph.links[slot.link];
|
||||
if (!link) continue;
|
||||
// connect this node output to the input of another node
|
||||
const originNode = app.graph.getNodeById(link.origin_id);
|
||||
originNode.connect(link.origin_slot, newNode, +innerInputId);
|
||||
@ -806,12 +836,23 @@ export class GroupNodeHandler {
|
||||
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
||||
if (optionIndex === -1) optionIndex = options.length;
|
||||
else optionIndex++;
|
||||
options.splice(optionIndex, 0, null, {
|
||||
content: "Convert to nodes",
|
||||
callback: () => {
|
||||
return this.convertToNodes();
|
||||
options.splice(
|
||||
optionIndex,
|
||||
0,
|
||||
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
|
||||
@ -865,6 +906,28 @@ export class GroupNodeHandler {
|
||||
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) {
|
||||
const handler = ({ detail }) => {
|
||||
const id = getId(detail);
|
||||
@ -927,13 +990,15 @@ export class GroupNodeHandler {
|
||||
continue;
|
||||
} else if (innerNode.type === "Reroute") {
|
||||
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
||||
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
||||
const node = this.innerNodes[targetNodeId];
|
||||
const input = node.inputs[targetSlot];
|
||||
if (input.widget) {
|
||||
const widget = node.widgets?.find((w) => w.name === input.widget.name);
|
||||
if (widget) {
|
||||
widget.value = newValue;
|
||||
if (rerouteLinks) {
|
||||
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
||||
const node = this.innerNodes[targetNodeId];
|
||||
const input = node.inputs[targetSlot];
|
||||
if (input.widget) {
|
||||
const widget = node.widgets?.find((w) => w.name === input.widget.name);
|
||||
if (widget) {
|
||||
widget.value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -975,7 +1040,7 @@ export class GroupNodeHandler {
|
||||
const [, , targetNodeId, targetNodeSlot] = link;
|
||||
const targetNode = this.groupData.nodeData.nodes[targetNodeId];
|
||||
const inputs = targetNode.inputs;
|
||||
const targetWidget = inputs?.[targetNodeSlot].widget;
|
||||
const targetWidget = inputs?.[targetNodeSlot]?.widget;
|
||||
if (!targetWidget) return;
|
||||
|
||||
const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
|
||||
@ -983,13 +1048,12 @@ export class GroupNodeHandler {
|
||||
if (v == null) return;
|
||||
|
||||
const widgetName = Object.values(map)[0];
|
||||
const widget = this.node.widgets.find(w => w.name === widgetName);
|
||||
if(widget) {
|
||||
const widget = this.node.widgets.find((w) => w.name === widgetName);
|
||||
if (widget) {
|
||||
widget.value = v;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
populateWidgets() {
|
||||
if (!this.node.widgets) return;
|
||||
|
||||
@ -1080,7 +1144,7 @@ export class GroupNodeHandler {
|
||||
}
|
||||
|
||||
static getGroupData(node) {
|
||||
return node.constructor?.nodeData?.[GROUP];
|
||||
return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP];
|
||||
}
|
||||
|
||||
static isGroupNode(node) {
|
||||
@ -1112,7 +1176,7 @@ export class GroupNodeHandler {
|
||||
}
|
||||
|
||||
function addConvertToGroupOptions() {
|
||||
function addOption(options, index) {
|
||||
function addConvertOption(options, index) {
|
||||
const selected = Object.values(app.canvas.selected_nodes ?? {});
|
||||
const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
|
||||
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
|
||||
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
const options = getCanvasMenuOptions.apply(this, arguments);
|
||||
const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
|
||||
addOption(options, index);
|
||||
addConvertOption(options, index);
|
||||
addManageOption(options, index + 1);
|
||||
return options;
|
||||
};
|
||||
|
||||
@ -1139,7 +1216,7 @@ function addConvertToGroupOptions() {
|
||||
const options = getNodeMenuOptions.apply(this, arguments);
|
||||
if (!GroupNodeHandler.isGroupNode(node)) {
|
||||
const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
|
||||
addOption(options, index);
|
||||
addConvertOption(options, index);
|
||||
}
|
||||
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": ".",
|
||||
"paths": {
|
||||
"/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"lib": ["DOM", "ES2022"]
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
|
@ -239,7 +239,8 @@ LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
|
||||
node.flags?.collapsed ||
|
||||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||
widget.computedHeight <= 0 ||
|
||||
widget.type === "converted-widget";
|
||||
widget.type === "converted-widget"||
|
||||
widget.type === "hidden";
|
||||
element.hidden = hidden;
|
||||
element.style.display = hidden ? "none" : null;
|
||||
if (hidden) {
|
||||
|
@ -4,6 +4,19 @@ import { ComfySettingsDialog } from "./ui/settings.js";
|
||||
|
||||
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) {
|
||||
const split = tag.split(".");
|
||||
const element = document.createElement(split.shift());
|
||||
@ -12,6 +25,11 @@ export function $el(tag, propsOrChildren, children) {
|
||||
}
|
||||
|
||||
if (propsOrChildren) {
|
||||
if (typeof propsOrChildren === "string") {
|
||||
propsOrChildren = { textContent: propsOrChildren };
|
||||
} else if (propsOrChildren instanceof Element) {
|
||||
propsOrChildren = [propsOrChildren];
|
||||
}
|
||||
if (Array.isArray(propsOrChildren)) {
|
||||
element.append(...propsOrChildren);
|
||||
} else {
|
||||
@ -35,7 +53,7 @@ export function $el(tag, propsOrChildren, children) {
|
||||
|
||||
Object.assign(element, propsOrChildren);
|
||||
if (children) {
|
||||
element.append(...children);
|
||||
element.append(...(children instanceof Array ? children : [children]));
|
||||
}
|
||||
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
span.drag-handle {
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
@ -160,12 +166,9 @@ body {
|
||||
letter-spacing: 2px;
|
||||
color: var(--drag-text);
|
||||
text-shadow: 1px 0 1px black;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.comfy-menu span.drag-handle::after {
|
||||
span.drag-handle::after {
|
||||
content: '.. .. ..';
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user