Skip to content

Commit 6e84a01

Browse files
cubiqmatt3o
andauthored
Refactor the template manager (comfyanonymous#1878)
* add drag-drop to node template manager * better dnd, save field on change * actually save templates --------- Co-authored-by: matt3o <matt3o@gmail.com>
1 parent dd116ab commit 6e84a01

File tree

1 file changed

+153
-84
lines changed

1 file changed

+153
-84
lines changed

web/extensions/core/nodeTemplates.js

Lines changed: 153 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { ComfyDialog, $el } from "../../scripts/ui.js";
1414
// To delete/rename:
1515
// Right click the canvas
1616
// Node templates -> Manage
17+
//
18+
// To rearrange:
19+
// Open the manage dialog and Drag and drop elements using the "Name:" label as handle
1720

1821
const id = "Comfy.NodeTemplates";
1922

@@ -22,6 +25,10 @@ class ManageTemplates extends ComfyDialog {
2225
super();
2326
this.element.classList.add("comfy-manage-templates");
2427
this.templates = this.load();
28+
this.draggedEl = null;
29+
this.saveVisualCue = null;
30+
this.emptyImg = new Image();
31+
this.emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
2532

2633
this.importInput = $el("input", {
2734
type: "file",
@@ -35,14 +42,11 @@ class ManageTemplates extends ComfyDialog {
3542

3643
createButtons() {
3744
const btns = super.createButtons();
38-
btns[0].textContent = "Cancel";
39-
btns.unshift(
40-
$el("button", {
41-
type: "button",
42-
textContent: "Save",
43-
onclick: () => this.save(),
44-
})
45-
);
45+
btns[0].textContent = "Close";
46+
btns[0].onclick = (e) => {
47+
clearTimeout(this.saveVisualCue);
48+
this.close();
49+
};
4650
btns.unshift(
4751
$el("button", {
4852
type: "button",
@@ -71,25 +75,6 @@ class ManageTemplates extends ComfyDialog {
7175
}
7276
}
7377

74-
save() {
75-
// Find all visible inputs and save them as our new list
76-
const inputs = this.element.querySelectorAll("input");
77-
const updated = [];
78-
79-
for (let i = 0; i < inputs.length; i++) {
80-
const input = inputs[i];
81-
if (input.parentElement.style.display !== "none") {
82-
const t = this.templates[i];
83-
t.name = input.value.trim() || input.getAttribute("data-name");
84-
updated.push(t);
85-
}
86-
}
87-
88-
this.templates = updated;
89-
this.store();
90-
this.close();
91-
}
92-
9378
store() {
9479
localStorage.setItem(id, JSON.stringify(this.templates));
9580
}
@@ -145,71 +130,155 @@ class ManageTemplates extends ComfyDialog {
145130
super.show(
146131
$el(
147132
"div",
148-
{
149-
style: {
150-
display: "grid",
151-
gridTemplateColumns: "1fr auto",
152-
gap: "5px",
153-
},
154-
},
155-
this.templates.flatMap((t) => {
133+
{},
134+
this.templates.flatMap((t,i) => {
156135
let nameInput;
157136
return [
158137
$el(
159-
"label",
138+
"div",
160139
{
161-
textContent: "Name: ",
140+
dataset: { id: i },
141+
className: "tempateManagerRow",
142+
style: {
143+
display: "grid",
144+
gridTemplateColumns: "1fr auto",
145+
border: "1px dashed transparent",
146+
gap: "5px",
147+
backgroundColor: "var(--comfy-menu-bg)"
148+
},
149+
ondragstart: (e) => {
150+
this.draggedEl = e.currentTarget;
151+
e.currentTarget.style.opacity = "0.6";
152+
e.currentTarget.style.border = "1px dashed yellow";
153+
e.dataTransfer.effectAllowed = 'move';
154+
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
155+
},
156+
ondragend: (e) => {
157+
e.target.style.opacity = "1";
158+
e.currentTarget.style.border = "1px dashed transparent";
159+
e.currentTarget.removeAttribute("draggable");
160+
161+
// rearrange the elements in the localStorage
162+
this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
163+
var prev_i = el.dataset.id;
164+
165+
if ( el == this.draggedEl && prev_i != i ) {
166+
[this.templates[i], this.templates[prev_i]] = [this.templates[prev_i], this.templates[i]];
167+
}
168+
el.dataset.id = i;
169+
});
170+
this.store();
171+
},
172+
ondragover: (e) => {
173+
e.preventDefault();
174+
if ( e.currentTarget == this.draggedEl )
175+
return;
176+
177+
let rect = e.currentTarget.getBoundingClientRect();
178+
if (e.clientY > rect.top + rect.height / 2) {
179+
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling);
180+
} else {
181+
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget);
182+
}
183+
}
162184
},
163185
[
164-
$el("input", {
165-
value: t.name,
166-
dataset: { name: t.name },
167-
$: (el) => (nameInput = el),
168-
}),
169-
]
170-
),
171-
$el(
172-
"div",
173-
{},
174-
[
175-
$el("button", {
176-
textContent: "Export",
177-
style: {
178-
fontSize: "12px",
179-
fontWeight: "normal",
180-
},
181-
onclick: (e) => {
182-
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
183-
const blob = new Blob([json], {type: "application/json"});
184-
const url = URL.createObjectURL(blob);
185-
const a = $el("a", {
186-
href: url,
187-
download: (nameInput.value || t.name) + ".json",
188-
style: {display: "none"},
189-
parent: document.body,
190-
});
191-
a.click();
192-
setTimeout(function () {
193-
a.remove();
194-
window.URL.revokeObjectURL(url);
195-
}, 0);
196-
},
197-
}),
198-
$el("button", {
199-
textContent: "Delete",
200-
style: {
201-
fontSize: "12px",
202-
color: "red",
203-
fontWeight: "normal",
204-
},
205-
onclick: (e) => {
206-
nameInput.value = "";
207-
e.target.parentElement.style.display = "none";
208-
e.target.parentElement.previousElementSibling.style.display = "none";
186+
$el(
187+
"label",
188+
{
189+
textContent: "Name: ",
190+
style: {
191+
cursor: "grab",
192+
},
193+
onmousedown: (e) => {
194+
// enable dragging only from the label
195+
if (e.target.localName == 'label')
196+
e.currentTarget.parentNode.draggable = 'true';
197+
}
209198
},
210-
}),
199+
[
200+
$el("input", {
201+
value: t.name,
202+
dataset: { name: t.name },
203+
style: {
204+
transitionProperty: 'background-color',
205+
transitionDuration: '0s',
206+
},
207+
onchange: (e) => {
208+
clearTimeout(this.saveVisualCue);
209+
var el = e.target;
210+
var row = el.parentNode.parentNode;
211+
this.templates[row.dataset.id].name = el.value.trim() || 'untitled';
212+
this.store();
213+
el.style.backgroundColor = 'rgb(40, 95, 40)';
214+
el.style.transitionDuration = '0s';
215+
this.saveVisualCue = setTimeout(function () {
216+
el.style.transitionDuration = '.7s';
217+
el.style.backgroundColor = 'var(--comfy-input-bg)';
218+
}, 15);
219+
},
220+
onkeypress: (e) => {
221+
var el = e.target;
222+
clearTimeout(this.saveVisualCue);
223+
el.style.transitionDuration = '0s';
224+
el.style.backgroundColor = 'var(--comfy-input-bg)';
225+
},
226+
$: (el) => (nameInput = el),
227+
})
228+
]
229+
),
230+
$el(
231+
"div",
232+
{},
233+
[
234+
$el("button", {
235+
textContent: "Export",
236+
style: {
237+
fontSize: "12px",
238+
fontWeight: "normal",
239+
},
240+
onclick: (e) => {
241+
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
242+
const blob = new Blob([json], {type: "application/json"});
243+
const url = URL.createObjectURL(blob);
244+
const a = $el("a", {
245+
href: url,
246+
download: (nameInput.value || t.name) + ".json",
247+
style: {display: "none"},
248+
parent: document.body,
249+
});
250+
a.click();
251+
setTimeout(function () {
252+
a.remove();
253+
window.URL.revokeObjectURL(url);
254+
}, 0);
255+
},
256+
}),
257+
$el("button", {
258+
textContent: "Delete",
259+
style: {
260+
fontSize: "12px",
261+
color: "red",
262+
fontWeight: "normal",
263+
},
264+
onclick: (e) => {
265+
const item = e.target.parentNode.parentNode;
266+
item.parentNode.removeChild(item);
267+
this.templates.splice(item.dataset.id*1, 1);
268+
this.store();
269+
// update the rows index, setTimeout ensures that the list is updated
270+
var that = this;
271+
setTimeout(function (){
272+
that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
273+
el.dataset.id = i;
274+
});
275+
}, 0);
276+
},
277+
}),
278+
]
279+
),
211280
]
212-
),
281+
)
213282
];
214283
})
215284
)

0 commit comments

Comments
 (0)