Spaces:
Paused
Paused
| import { app } from "../../scripts/app.js"; | |
| import { ComfyDialog, $el } from "../../scripts/ui.js"; | |
| // Adds the ability to save and add multiple nodes as a template | |
| // To save: | |
| // Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes) | |
| // Right click the canvas | |
| // Save Node Template -> give it a name | |
| // | |
| // To add: | |
| // Right click the canvas | |
| // Node templates -> click the one to add | |
| // | |
| // To delete/rename: | |
| // Right click the canvas | |
| // Node templates -> Manage | |
| // | |
| // To rearrange: | |
| // Open the manage dialog and Drag and drop elements using the "Name:" label as handle | |
| const id = "Comfy.NodeTemplates"; | |
| class ManageTemplates extends ComfyDialog { | |
| constructor() { | |
| super(); | |
| this.element.classList.add("comfy-manage-templates"); | |
| this.templates = this.load(); | |
| this.draggedEl = null; | |
| this.saveVisualCue = null; | |
| this.emptyImg = new Image(); | |
| this.emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; | |
| this.importInput = $el("input", { | |
| type: "file", | |
| accept: ".json", | |
| multiple: true, | |
| style: {display: "none"}, | |
| parent: document.body, | |
| onchange: () => this.importAll(), | |
| }); | |
| } | |
| createButtons() { | |
| const btns = super.createButtons(); | |
| btns[0].textContent = "Close"; | |
| btns[0].onclick = (e) => { | |
| clearTimeout(this.saveVisualCue); | |
| this.close(); | |
| }; | |
| btns.unshift( | |
| $el("button", { | |
| type: "button", | |
| textContent: "Export", | |
| onclick: () => this.exportAll(), | |
| }) | |
| ); | |
| btns.unshift( | |
| $el("button", { | |
| type: "button", | |
| textContent: "Import", | |
| onclick: () => { | |
| this.importInput.click(); | |
| }, | |
| }) | |
| ); | |
| return btns; | |
| } | |
| load() { | |
| const templates = localStorage.getItem(id); | |
| if (templates) { | |
| return JSON.parse(templates); | |
| } else { | |
| return []; | |
| } | |
| } | |
| store() { | |
| localStorage.setItem(id, JSON.stringify(this.templates)); | |
| } | |
| async importAll() { | |
| for (const file of this.importInput.files) { | |
| if (file.type === "application/json" || file.name.endsWith(".json")) { | |
| const reader = new FileReader(); | |
| reader.onload = async () => { | |
| var importFile = JSON.parse(reader.result); | |
| if (importFile && importFile?.templates) { | |
| for (const template of importFile.templates) { | |
| if (template?.name && template?.data) { | |
| this.templates.push(template); | |
| } | |
| } | |
| this.store(); | |
| } | |
| }; | |
| await reader.readAsText(file); | |
| } | |
| } | |
| this.importInput.value = null; | |
| this.close(); | |
| } | |
| exportAll() { | |
| if (this.templates.length == 0) { | |
| alert("No templates to export."); | |
| return; | |
| } | |
| const json = JSON.stringify({templates: this.templates}, null, 2); // convert the data to a JSON string | |
| const blob = new Blob([json], {type: "application/json"}); | |
| const url = URL.createObjectURL(blob); | |
| const a = $el("a", { | |
| href: url, | |
| download: "node_templates.json", | |
| style: {display: "none"}, | |
| parent: document.body, | |
| }); | |
| a.click(); | |
| setTimeout(function () { | |
| a.remove(); | |
| window.URL.revokeObjectURL(url); | |
| }, 0); | |
| } | |
| show() { | |
| // Show list of template names + delete button | |
| super.show( | |
| $el( | |
| "div", | |
| {}, | |
| this.templates.flatMap((t,i) => { | |
| let nameInput; | |
| return [ | |
| $el( | |
| "div", | |
| { | |
| dataset: { id: i }, | |
| className: "tempateManagerRow", | |
| style: { | |
| display: "grid", | |
| gridTemplateColumns: "1fr auto", | |
| border: "1px dashed transparent", | |
| gap: "5px", | |
| backgroundColor: "var(--comfy-menu-bg)" | |
| }, | |
| ondragstart: (e) => { | |
| this.draggedEl = e.currentTarget; | |
| e.currentTarget.style.opacity = "0.6"; | |
| e.currentTarget.style.border = "1px dashed yellow"; | |
| e.dataTransfer.effectAllowed = 'move'; | |
| e.dataTransfer.setDragImage(this.emptyImg, 0, 0); | |
| }, | |
| ondragend: (e) => { | |
| e.target.style.opacity = "1"; | |
| e.currentTarget.style.border = "1px dashed transparent"; | |
| e.currentTarget.removeAttribute("draggable"); | |
| // rearrange the elements in the localStorage | |
| this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { | |
| var prev_i = el.dataset.id; | |
| if ( el == this.draggedEl && prev_i != i ) { | |
| [this.templates[i], this.templates[prev_i]] = [this.templates[prev_i], this.templates[i]]; | |
| } | |
| el.dataset.id = i; | |
| }); | |
| this.store(); | |
| }, | |
| ondragover: (e) => { | |
| e.preventDefault(); | |
| if ( e.currentTarget == this.draggedEl ) | |
| return; | |
| let rect = e.currentTarget.getBoundingClientRect(); | |
| if (e.clientY > rect.top + rect.height / 2) { | |
| e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling); | |
| } else { | |
| e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget); | |
| } | |
| } | |
| }, | |
| [ | |
| $el( | |
| "label", | |
| { | |
| textContent: "Name: ", | |
| style: { | |
| cursor: "grab", | |
| }, | |
| onmousedown: (e) => { | |
| // enable dragging only from the label | |
| if (e.target.localName == 'label') | |
| e.currentTarget.parentNode.draggable = 'true'; | |
| } | |
| }, | |
| [ | |
| $el("input", { | |
| value: t.name, | |
| dataset: { name: t.name }, | |
| style: { | |
| transitionProperty: 'background-color', | |
| transitionDuration: '0s', | |
| }, | |
| onchange: (e) => { | |
| clearTimeout(this.saveVisualCue); | |
| var el = e.target; | |
| var row = el.parentNode.parentNode; | |
| this.templates[row.dataset.id].name = el.value.trim() || 'untitled'; | |
| this.store(); | |
| el.style.backgroundColor = 'rgb(40, 95, 40)'; | |
| el.style.transitionDuration = '0s'; | |
| this.saveVisualCue = setTimeout(function () { | |
| el.style.transitionDuration = '.7s'; | |
| el.style.backgroundColor = 'var(--comfy-input-bg)'; | |
| }, 15); | |
| }, | |
| onkeypress: (e) => { | |
| var el = e.target; | |
| clearTimeout(this.saveVisualCue); | |
| el.style.transitionDuration = '0s'; | |
| el.style.backgroundColor = 'var(--comfy-input-bg)'; | |
| }, | |
| $: (el) => (nameInput = el), | |
| }) | |
| ] | |
| ), | |
| $el( | |
| "div", | |
| {}, | |
| [ | |
| $el("button", { | |
| textContent: "Export", | |
| style: { | |
| fontSize: "12px", | |
| fontWeight: "normal", | |
| }, | |
| onclick: (e) => { | |
| const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string | |
| const blob = new Blob([json], {type: "application/json"}); | |
| const url = URL.createObjectURL(blob); | |
| const a = $el("a", { | |
| href: url, | |
| download: (nameInput.value || t.name) + ".json", | |
| style: {display: "none"}, | |
| parent: document.body, | |
| }); | |
| a.click(); | |
| setTimeout(function () { | |
| a.remove(); | |
| window.URL.revokeObjectURL(url); | |
| }, 0); | |
| }, | |
| }), | |
| $el("button", { | |
| textContent: "Delete", | |
| style: { | |
| fontSize: "12px", | |
| color: "red", | |
| fontWeight: "normal", | |
| }, | |
| onclick: (e) => { | |
| const item = e.target.parentNode.parentNode; | |
| item.parentNode.removeChild(item); | |
| this.templates.splice(item.dataset.id*1, 1); | |
| this.store(); | |
| // update the rows index, setTimeout ensures that the list is updated | |
| var that = this; | |
| setTimeout(function (){ | |
| that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { | |
| el.dataset.id = i; | |
| }); | |
| }, 0); | |
| }, | |
| }), | |
| ] | |
| ), | |
| ] | |
| ) | |
| ]; | |
| }) | |
| ) | |
| ); | |
| } | |
| } | |
| app.registerExtension({ | |
| name: id, | |
| setup() { | |
| const manage = new ManageTemplates(); | |
| const clipboardAction = (cb) => { | |
| // We use the clipboard functions but dont want to overwrite the current user clipboard | |
| // Restore it after we've run our callback | |
| const old = localStorage.getItem("litegrapheditor_clipboard"); | |
| cb(); | |
| localStorage.setItem("litegrapheditor_clipboard", old); | |
| }; | |
| const orig = LGraphCanvas.prototype.getCanvasMenuOptions; | |
| LGraphCanvas.prototype.getCanvasMenuOptions = function () { | |
| const options = orig.apply(this, arguments); | |
| options.push(null); | |
| options.push({ | |
| content: `Save Selected as Template`, | |
| disabled: !Object.keys(app.canvas.selected_nodes || {}).length, | |
| callback: () => { | |
| const name = prompt("Enter name"); | |
| if (!name || !name.trim()) return; | |
| clipboardAction(() => { | |
| app.canvas.copyToClipboard(); | |
| manage.templates.push({ | |
| name, | |
| data: localStorage.getItem("litegrapheditor_clipboard"), | |
| }); | |
| manage.store(); | |
| }); | |
| }, | |
| }); | |
| // Map each template to a menu item | |
| const subItems = manage.templates.map((t) => ({ | |
| content: t.name, | |
| callback: () => { | |
| clipboardAction(() => { | |
| localStorage.setItem("litegrapheditor_clipboard", t.data); | |
| app.canvas.pasteFromClipboard(); | |
| }); | |
| }, | |
| })); | |
| subItems.push(null, { | |
| content: "Manage", | |
| callback: () => manage.show(), | |
| }); | |
| options.push({ | |
| content: "Node Templates", | |
| submenu: { | |
| options: subItems, | |
| }, | |
| }); | |
| return options; | |
| }; | |
| }, | |
| }); | |