Spaces:
Configuration error
Configuration error
| 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; | |
| } | |
| } | |
| }); |