Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
});
|