import { app } from "../../../../scripts/app.js"; import { ComfyWidgets } from "../../../../scripts/widgets.js"; const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 }; const WIDGET_GAP = -4; function hideInfoWidget(e, node, widget) { let dropdownShouldBeRemoved = false; let selectionIndex = -1; if (e) { e.preventDefault(); e.stopPropagation(); displayDropdown(widget); } else { hideWidget(widget, node); } function createDropdownElement() { const dropdown = document.createElement('ul'); dropdown.id = 'hideinfo-dropdown'; dropdown.setAttribute('role', 'listbox'); dropdown.classList.add('hideInfo-dropdown'); return dropdown; } function createDropdownItem(textContent, action) { const listItem = document.createElement('li'); listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`; listItem.classList.add('hideInfo-item'); listItem.setAttribute('role', 'option'); listItem.textContent = textContent; listItem.addEventListener('mousedown', (event) => { event.preventDefault(); action(widget, node); // perform the action when dropdown item is clicked removeDropdown(); dropdownShouldBeRemoved = false; }); listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute return listItem; } function displayDropdown(widget) { removeDropdown(); const dropdown = createDropdownElement(); const listItemHide = createDropdownItem('Hide info Widget', hideWidget); const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype); dropdown.appendChild(listItemHide); dropdown.appendChild(listItemHideAll); const inputRect = widget.inputEl.getBoundingClientRect(); dropdown.style.top = `${inputRect.top + inputRect.height}px`; dropdown.style.left = `${inputRect.left}px`; dropdown.style.width = `${inputRect.width}px`; document.body.appendChild(dropdown); dropdownShouldBeRemoved = true; widget.inputEl.removeEventListener('keydown', handleKeyDown); widget.inputEl.addEventListener('keydown', handleKeyDown); document.addEventListener('click', handleDocumentClick); } function removeDropdown() { const dropdown = document.getElementById('hideinfo-dropdown'); if (dropdown) { dropdown.remove(); widget.inputEl.removeEventListener('keydown', handleKeyDown); } document.removeEventListener('click', handleDocumentClick); } function handleKeyDown(event) { const dropdownItems = document.querySelectorAll('.hideInfo-item'); if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) { event.preventDefault(); if (selectionIndex !== -1) { const selectedAction = dropdownItems[selectionIndex].dataset.action; if (selectedAction === 'HideinfoWidget') { hideWidget(widget, node); } else if (selectedAction === 'Hideforall') { hideWidgetForNodetype(widget, node); } removeDropdown(); dropdownShouldBeRemoved = false; } } else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) { event.preventDefault(); if (selectionIndex !== -1) { dropdownItems[selectionIndex].classList.remove('selected'); } selectionIndex = (selectionIndex + 1) % dropdownItems.length; dropdownItems[selectionIndex].classList.add('selected'); } else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) { event.preventDefault(); if (selectionIndex !== -1) { dropdownItems[selectionIndex].classList.remove('selected'); } selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length; dropdownItems[selectionIndex].classList.add('selected'); } else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) { event.preventDefault(); removeDropdown(); } } function hideWidget(widget, node) { node.properties['infoWidgetHidden'] = true; widget.type = "esayHidden"; widget.computeSize = () => [0, WIDGET_GAP]; node.setSize([node.size[0], node.size[1]]); } function hideWidgetForNodetype(widget, node) { hideWidget(widget, node) const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]"); if (!hiddenNodeTypes.includes(node.constructor.type)) { hiddenNodeTypes.push(node.constructor.type); } localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes)); } function handleDocumentClick(event) { const dropdown = document.getElementById('hideinfo-dropdown'); // If the click was outside the dropdown and the dropdown should be removed, remove it if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) { removeDropdown(); dropdownShouldBeRemoved = false; } } } var styleElement = document.createElement("style"); const cssCode = ` .easy-info_widget { background-color: var(--comfy-input-bg); color: var(--input-text); overflow: hidden; padding: 2px; resize: none; border: none; box-sizing: border-box; font-size: 10px; border-radius: 7px; text-align: center; text-wrap: balance; } .hideInfo-dropdown { position: absolute; box-sizing: border-box; background-color: #121212; border-radius: 7px; box-shadow: 0 2px 4px rgba(255, 255, 255, .25); padding: 0; margin: 0; list-style: none; z-index: 1000; overflow: auto; max-height: 200px; } .hideInfo-dropdown li { padding: 4px 10px; cursor: pointer; font-family: system-ui; font-size: 0.7rem; } .hideInfo-dropdown li:hover, .hideInfo-dropdown li.selected { background-color: #e5e5e5; border-radius: 7px; } ` styleElement.innerHTML = cssCode document.head.appendChild(styleElement); const InfoSymbol = Symbol(); const InfoResizeSymbol = Symbol(); // WIDGET FUNCTIONS function addInfoWidget(node, name, opts, app) { const INFO_W_SIZE = 50; node.addProperty('infoWidgetHidden', false) function computeSize(size) { if (node.widgets[0].last_y == null) return; let y = node.widgets[0].last_y; // Compute the height of all non easyInfo widgets let widgetHeight = 0; const infoWidges = []; for (let i = 0; i < node.widgets.length; i++) { const w = node.widgets[i]; if (w.type === "easyInfo") { infoWidges.push(w); } else { if (w.computeSize) { widgetHeight += w.computeSize()[1] + 4; } else { widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; } } } let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets // Check if there's enough space for all widgets if (size[1] < y + widgetHeight + infoWidgetSpace) { // There isn't enough space for all the widgets, increase the size of the node node.size[1] = y + widgetHeight + infoWidgetSpace; node.graph.setDirtyCanvas(true); } // Position each of the widgets for (const w of node.widgets) { w.y = y; if (w.type === "easyInfo") { y += INFO_W_SIZE; } else if (w.computeSize) { y += w.computeSize()[1] + 4; } else { y += LiteGraph.NODE_WIDGET_HEIGHT + 4; } } } const widget = { type: "easyInfo", name, get value() { return this.inputEl.value; }, set value(x) { this.inputEl.value = x; }, draw: function (ctx, _, widgetWidth, y, widgetHeight) { if (!this.parent.inputHeight) { // If we are initially offscreen when created we wont have received a resize event // Calculate it here instead computeSize(node.size); } const visible = app.canvas.ds.scale > 0.5 && this.type === "easyInfo"; const margin = 10; const elRect = ctx.canvas.getBoundingClientRect(); const transform = new DOMMatrix() .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) .multiplySelf(ctx.getTransform()) .translateSelf(margin, margin + y); Object.assign(this.inputEl.style, { transformOrigin: "0 0", transform: transform, left: "0px", top: "0px", width: `${widgetWidth - (margin * 2)}px`, height: `${this.parent.inputHeight - (margin * 2)}px`, position: "absolute", background: (!node.color)?'':node.color, color: (!node.color)?'':'white', zIndex: app.graph._nodes.indexOf(node), }); this.inputEl.hidden = !visible; }, }; widget.inputEl = document.createElement("textarea"); widget.inputEl.className = "easy-info_widget"; widget.inputEl.value = opts.defaultVal; widget.inputEl.placeholder = opts.placeholder || ""; widget.inputEl.readOnly = true; widget.parent = node; document.body.appendChild(widget.inputEl); node.addCustomWidget(widget); app.canvas.onDrawBackground = function () { // Draw node isnt fired once the node is off the screen // if it goes off screen quickly, the input may not be removed // this shifts it off screen so it can be moved back if the node is visible. for (let n in app.graph._nodes) { n = app.graph._nodes[n]; for (let w in n.widgets) { let wid = n.widgets[w]; if (Object.hasOwn(wid, "inputEl")) { wid.inputEl.style.left = -8000 + "px"; wid.inputEl.style.position = "absolute"; } } } }; node.onRemoved = function () { // When removing this node we need to remove the input from the DOM for (let y in this.widgets) { if (this.widgets[y].inputEl) { this.widgets[y].inputEl.remove(); } } }; widget.onRemove = () => { widget.inputEl?.remove(); // Restore original size handler if we are the last if (!--node[InfoSymbol]) { node.onResize = node[InfoResizeSymbol]; delete node[InfoSymbol]; delete node[InfoResizeSymbol]; } }; if (node[InfoSymbol]) { node[InfoSymbol]++; } else { node[InfoSymbol] = 1; const onResize = (node[InfoResizeSymbol] = node.onResize); node.onResize = function (size) { computeSize(size); // Call original resizer handler if (onResize) { console.log(this, arguments) onResize.apply(this, arguments); } }; } return { widget }; } // WIDGETS const easyCustomWidgets = { INFO(node, inputName, inputData, app) { const defaultVal = inputData[1].default || ""; return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app); }, } app.registerExtension({ name: "comfy.easy.widgets", getCustomWidgets(app) { return easyCustomWidgets; }, nodeCreated(node) { if (node.widgets) { // Locate info widgets const widgets = node.widgets.filter((n) => (n.type === "easyInfo")); for (const widget of widgets) { widget.inputEl.addEventListener('contextmenu', function(e) { hideInfoWidget(e, node, widget); }); widget.inputEl.addEventListener('click', function(e) { hideInfoWidget(e, node, widget); }); } } }, async beforeRegisterNodeDef(nodeType, nodeData, app) { const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]"); const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function () { const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; if (this.properties['infoWidgetHidden']) { for (let i in this.widgets) { if (this.widgets[i].type == "easyInfo") { hideInfoWidget(null, this, this.widgets[i]); } } } return r; }; const origOnAdded = nodeType.prototype.onAdded; nodeType.prototype.onAdded = function () { const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined; if (hiddenNodeTypes.includes(this.type)) { for (let i in this.widgets) { if (this.widgets[i].type == "easyInfo") { this.properties['infoWidgetHidden'] = true; } } } return r; } } });