Files
tusharbhutt-Endless-Buttons/web/endless_node_loader.js
T
2025-07-20 13:40:16 -06:00

1020 lines
34 KiB
JavaScript

import { registerEndlessTool } from './endless_ui_helpers.js';
console.log("✅ Endless Node Loader loaded.");
let dialogInstance = null;
// Set search and recent Nodes
let recentlyUsedNodes = JSON.parse(localStorage.getItem('endlessNodeLoader_recentlyUsed') || '[]');
let searchHistory = JSON.parse(localStorage.getItem('endlessNodeLoader_searchHistory') || '[]');
// Timeout for search history drop down
let searchTimeout;
let hoverTimeout;
// Dragging state
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
// Filter state
let currentFilter = '';
let allNodesData = [];
function handleDrag(e) {
if (!isDragging) return;
const container = dialogInstance;
if (!container) return;
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
container.style.left = `${Math.max(0, Math.min(window.innerWidth - container.offsetWidth, x))}px`;
container.style.top = `${Math.max(0, Math.min(window.innerHeight - container.offsetHeight, y))}px`;
}
function handleDragEnd() {
isDragging = false;
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleDragEnd);
document.body.style.userSelect = "";
}
// Get ComfyUI themes //
function getComfyUIThemeColors() {
const computed = getComputedStyle(document.documentElement);
const getVar = name => computed.getPropertyValue(name).trim() || null;
return {
background: getVar("--bg-color"),
surface: getVar("--comfy-menu-bg"),
surfaceSecondary: getVar("--comfy-menu-secondary-bg"),
inputBg: getVar("--comfy-input-bg"),
inputText: getVar("--input-text"),
textSecondary: getVar("--descrip-text"),
border: getVar("--border-color"),
accent: getVar("--comfy-accent") || "#4a90e2",
hoverBg: getVar("--content-hover-bg"),
hoverFg: getVar("--content-hover-fg"),
shadow: getVar("--bar-shadow")
};
}
function addDialogBoxParts() {
const styleId = 'endless-node-loader-styles';
if (document.getElementById(styleId)) return;
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.dialog-container {
display: flex;
flex-direction: column;
height: 60vh;
width: 35vw;
background-color: var(--comfy-menu-bg);
color: var(--input-text);
padding: 10px;
border: 2px solid var(--border-color);
position: fixed;
top: 10%;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
border-radius: 8px;
box-shadow: var(--bar-shadow, 0 4px 20px rgba(0,0,0,0.5));
pointer-events: auto;
overflow: hidden;
box-sizing: border-box;
}
.dialog-title {
margin: 0 0 15px 0;
cursor: move;
user-select: none;
padding: 4px;
background-color: var(--comfy-menu-secondary-bg);
color: var(--input-text);
border-radius: 4px;
font-size: 14px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
margin-bottom: 6px;
}
.dialog-filter-section {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.filter-input-container {
position: relative;
display: flex;
gap: 8px;
align-items: center;
}
.filter-input {
flex: 1;
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px 8px;
font-size: 12px;
}
.filter-input:focus {
outline: none;
border-color: var(--comfy-accent);
}
.filter-expand-btn {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.filter-expand-btn:hover {
background-color: var(--content-hover-bg);
}
.search-history-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--comfy-menu-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
max-height: 1050px;
overflow-y: auto;
z-index: 10000;
display: none;
}
.search-history-item {
padding: 6px 8px;
cursor: pointer;
font-size: 12px;
border-bottom: 1px solid var(--border-color);
}
.search-history-item:last-child {
border-bottom: none;
}
.search-history-item:hover {
background-color: var(--comfy-input-bg);
}
.node-counters {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--descrip-text);
}
.counter-selected {
color: var(--comfy-accent);
font-weight: bold;
}
.dialog-recent {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 30px;
max-height: 15%;
overflow-y: auto;
box-sizing: border-box;
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
margin-bottom: 6px;
}
.recent-chip {
display: inline-block;
background-color: var(--comfy-menu-secondary-bg);
color: var(--input-text);
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
white-space: nowrap;
flex: 0 0 auto;
border: 1px solid transparent;
background-image: linear-gradient(rgba(128, 128, 255, 0.08), rgba(128, 128, 255, 0.08));
}
.recent-chip:hover {
border-color: var(--comfy-accent);
background-color: var(--comfy-input-bg);
background-image: linear-gradient(rgba(128, 128, 255, 0.12), rgba(128, 128, 255, 0.12));
transition: border-color 0.2s ease;
}
.dialog-list {
flex: 1 1 auto;
min-height: 40%;
overflow-y: auto;
box-sizing: border-box;
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
margin-bottom: 6px;
}
.node-details {
margin-bottom: 4px;
}
.node-details > summary {
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
list-style: none;
position: relative;
color: var(--input-text);
}
.node-details > summary::-webkit-details-marker {
display: none;
}
.node-details > summary::before {
content: "▶";
display: inline-block;
width: 12px;
text-align: center;
color: var(--descrip-text);
font-size: 10px;
transition: transform 0.2s ease;
margin-right: 4px;
}
.node-details[open] > summary::before {
transform: rotate(90deg);
color: var(--input-text);
}
.node-details > summary:hover::before {
color: var(--input-text);
}
.node-details > summary:hover {
background-color: var(--comfy-input-bg);
border-radius: 4px;
}
.node-details ul {
margin: 4px 0;
padding-left: 1em;
}
.node-details li:hover {
background-color: var(--comfy-menu-secondary-bg);
border-radius: 4px;
}
.node-details input[type="checkbox"] {
accent-color: var(--comfy-accent);
}
.node-details button {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
}
.node-details button:hover {
background-color: var(--content-hover-bg);
}
.node-details .select-all-btn {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
background-image: linear-gradient(rgba(128, 255, 128, 0.08), rgba(128, 255, 128, 0.08));
}
.node-details .select-all-btn:hover {
background-color: var(--content-hover-bg);
border-color: var(--comfy-accent);
background-image: linear-gradient(rgba(128, 255, 128, 0.12), rgba(128, 255, 128, 0.12));
transition: border-color 0.2s ease;
}
.node-details .select-none-btn {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
background-image: linear-gradient(rgba(255, 128, 128, 0.08), rgba(255, 128, 128, 0.08));
}
.node-details .select-none-btn:hover {
background-color: var(--content-hover-bg);
border-color: var(--comfy-accent);
background-image: linear-gradient(rgba(255, 128, 128, 0.12), rgba(255, 128, 128, 0.12));
transition: border-color 0.2s ease;
}
.dialog-footer {
display: flex;
justify-content: space-between;
padding: 8px;
gap: 8px;
}
.dialog-btn {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
}
.dialog-btn:hover {
background-color: var(--content-hover-bg);
border-color: var(--comfy-accent);
transition: border-color 0.2s ease;
}
.dialog-btn.primary {
background-color: var(--comfy-input-bg);
color: var(--input-text);
background-image: linear-gradient(rgba(128, 255, 128, 0.08), rgba(128, 255, 128, 0.08));
border: 1px solid rgba(128, 255, 128, 0.12);
}
.dialog-btn.primary:hover {
opacity: 0.9;
background-color: var(--content-hover-bg);
border-color: var(--comfy-accent);
transition: border-color 0.2s ease;
background-image: linear-gradient(rgba(128, 255, 128, 0.12), rgba(128, 255, 128, 0.12));
}
.dialog-btn.sec {
background-color: var(--comfy-input-bg);
color: var(--input-text);
background-image: linear-gradient(rgba(255, 128, 128, 0.08), rgba(255, 128, 128, 0.08));
border: 1px solid rgba(255, 128, 128, 0.12);
}
.dialog-btn.sec:hover {
opacity: 0.9;
background-color: var(--content-hover-bg);
border-color: var(--comfy-accent);
transition: border-color 0.2s ease;
background-image: linear-gradient(rgba(255, 128, 128, 0.12), rgba(255, 128, 128, 0.12));
}
.hidden {
display: none !important;
}
`;
document.head.appendChild(style);
}
// Node spawning functions
function spawnNodes(types, spacingX = 300, spacingY = 150) {
if (!window.app?.graph?.add) {
alert("ComfyUI graph not available.");
return;
}
const startX = -app.canvas.ds.offset[0] + 50;
const startY = -app.canvas.ds.offset[1] + 50;
const existingPositions = getExistingNodePositions();
const spawnedNodes = [];
types.forEach((type, i) => {
const node = LiteGraph.createNode(type);
if (node) {
const nodeWidth = node.size ? node.size[0] : 200;
const nodeHeight = node.size ? node.size[1] : 100;
// Use collision detection
const position = findNonOverlappingPosition(
startX + (i % 5) * spacingX,
startY + Math.floor(i / 5) * spacingY,
nodeWidth,
nodeHeight,
existingPositions,
spacingX,
spacingY
);
node.pos = [position.x, position.y];
app.graph.add(node);
spawnedNodes.push(type);
// Add this node's position to existing positions for next iteration
existingPositions.push({
x: position.x,
y: position.y,
width: nodeWidth,
height: nodeHeight
});
} else {
console.warn(`Could not create node: ${type}`);
}
});
// Update recently used nodes
updateRecentlyUsedNodes(spawnedNodes);
app.graph.setDirtyCanvas(true, true);
}
function getExistingNodePositions() {
const positions = [];
if (window.app?.graph?.nodes) {
window.app.graph.nodes.forEach(node => {
if (node.pos) {
positions.push({
x: node.pos[0],
y: node.pos[1],
width: node.size ? node.size[0] : 200,
height: node.size ? node.size[1] : 100
});
}
});
}
return positions;
}
function findNonOverlappingPosition(startX, startY, width, height, existingPositions, spacingX, spacingY) {
const padding = 20;
let x = startX;
let y = startY;
while (true) {
let overlaps = false;
for (const pos of existingPositions) {
if (!(x + width + padding < pos.x ||
x - padding > pos.x + pos.width ||
y + height + padding < pos.y ||
y - padding > pos.y + pos.height)) {
overlaps = true;
break;
}
}
if (!overlaps) {
return { x, y };
}
// Move to next position
x += spacingX;
if (x > startX + spacingX * 5) { // After 5 columns, move to next row
x = startX;
y += spacingY;
}
}
}
// Recently used nodes management
function updateRecentlyUsedNodes(newNodes) {
newNodes.forEach(nodeType => {
// Remove if already exists
const index = recentlyUsedNodes.indexOf(nodeType);
if (index > -1) {
recentlyUsedNodes.splice(index, 1);
}
// Add to beginning
recentlyUsedNodes.unshift(nodeType);
});
// Keep only last 15
recentlyUsedNodes = recentlyUsedNodes.slice(0, 15);
localStorage.setItem('endlessNodeLoader_recentlyUsed', JSON.stringify(recentlyUsedNodes));
// Update chips if dialog is open
if (dialogInstance) {
updateRecentChips();
}
}
function updateRecentChips() {
const recentSection = dialogInstance.querySelector('.dialog-recent');
if (!recentSection) return;
recentSection.innerHTML = '';
recentlyUsedNodes.forEach(nodeType => {
const chip = document.createElement('button');
chip.className = 'recent-chip';
// Get display name
const nodeClass = LiteGraph.registered_node_types[nodeType];
const displayName = nodeClass?.title || nodeClass?.name || nodeType.split("/").pop();
chip.textContent = displayName;
chip.title = nodeType; // Full type as tooltip
chip.onclick = () => {
// Check this node type in the list
const checkbox = Array.from(dialogInstance.querySelectorAll('.node-checkbox')).find(cb => {
return cb.closest('li').dataset.nodeType === nodeType;
});
if (checkbox) {
checkbox.checked = true;
updateSelectedCounter();
}
};
recentSection.appendChild(chip);
});
}
// Search history management
function addToSearchHistory(searchTerm) {
if (!searchTerm.trim() || searchHistory.includes(searchTerm)) return;
searchHistory.unshift(searchTerm);
searchHistory = searchHistory.slice(0, 15);
localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
}
function showSearchHistory(inputElement) {
const dropdown = inputElement.parentElement.querySelector('.search-history-dropdown');
if (!dropdown || searchHistory.length === 0) {
if (dropdown) dropdown.style.display = 'none';
return;
}
dropdown.innerHTML = '';
searchHistory.forEach(term => {
const item = document.createElement('div');
item.className = 'search-history-item';
item.textContent = term;
item.onclick = () => {
inputElement.value = term;
applyFilter(term, true); // Save this selection to history
hideSearchHistory(dropdown);
};
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
// Auto-hide after 10 seconds if no interaction
setTimeout(() => {
if (dropdown.style.display === 'block') {
hideSearchHistory(dropdown);
}
}, 10000);
}
function hideSearchHistory(dropdown) {
dropdown.style.display = 'none';
}
// Filter functionality
function applyFilter(filterText, saveToHistory = true) {
currentFilter = filterText.toLowerCase();
const nodeListSection = dialogInstance.querySelector('.dialog-list');
if (!currentFilter) {
// Show all
nodeListSection.querySelectorAll('.node-details, .node-details li').forEach(el => {
el.classList.remove('hidden');
});
updateTotalCounter();
return;
}
// Filter logic
nodeListSection.querySelectorAll('.node-details').forEach(details => {
const categoryName = details.querySelector('summary span').textContent.toLowerCase();
const categoryMatches = categoryName.includes(currentFilter);
let hasMatchingNodes = false;
const nodeItems = details.querySelectorAll('li');
nodeItems.forEach(li => {
const nodeText = li.textContent.toLowerCase();
const nodeType = li.dataset.nodeType?.toLowerCase() || '';
const matches = nodeText.includes(currentFilter) || nodeType.includes(currentFilter);
if (matches) {
li.classList.remove('hidden');
hasMatchingNodes = true;
} else {
li.classList.add('hidden');
}
});
// Show category if it matches or has matching nodes
if (categoryMatches || hasMatchingNodes) {
details.classList.remove('hidden');
if (hasMatchingNodes && !categoryMatches) {
details.open = true; // Auto-expand if contains matches
}
} else {
details.classList.add('hidden');
}
});
updateTotalCounter();
}
// Counter management
function updateSelectedCounter() {
const counter = dialogInstance.querySelector('.counter-selected');
if (!counter) return;
const checkedBoxes = dialogInstance.querySelectorAll('.node-checkbox:checked');
counter.textContent = `Selected: ${checkedBoxes.length}`;
}
function updateTotalCounter() {
const counter = dialogInstance.querySelector('.counter-total');
if (!counter) return;
const visibleNodes = dialogInstance.querySelectorAll('.node-details li:not(.hidden)');
const totalNodes = allNodesData.length;
counter.textContent = `Total: ${visibleNodes.length}/${totalNodes}`;
}
// Expand/collapse functionality
function toggleAllCategories(expand) {
const details = dialogInstance.querySelectorAll('.node-details:not(.hidden)');
details.forEach(detail => {
detail.open = expand;
});
}
// Build node list and hierarchy //
function buildHierarchy(nodes) {
const root = {};
nodes.forEach(n => {
let current = root;
n.pathParts.forEach((part, idx) => {
if (!current[part]) {
current[part] = { _nodes: [], _subcategories: {} };
}
if (idx === n.pathParts.length - 1) {
current[part]._nodes.push(n);
} else {
current = current[part]._subcategories;
}
});
});
return root;
}
function countNodesInCategory(categoryObj) {
let count = 0;
count += categoryObj._nodes ? categoryObj._nodes.length : 0;
if (categoryObj._subcategories) {
Object.values(categoryObj._subcategories).forEach(sub => {
count += countNodesInCategory(sub);
});
}
return count;
}
function selectAllInCategory(categoryDetails, select = true) {
const items = categoryDetails.querySelectorAll("input[type='checkbox']");
items.forEach(checkbox => {
checkbox.checked = select;
});
updateSelectedCounter();
}
function renderCategory(categoryObj, depth = 0) {
return Object.entries(categoryObj)
.sort(([a], [b]) => a.localeCompare(b))
.map(([cat, obj]) => {
const totalNodes = countNodesInCategory(obj);
const details = document.createElement("details");
details.open = false;
details.className = "node-details";
details.style.paddingLeft = `${depth * 1.2}em`;
const summary = document.createElement("summary");
const categoryName = document.createElement("span");
categoryName.textContent = `${cat} (${totalNodes})`;
const selectAllBtn = document.createElement("button");
selectAllBtn.textContent = "All";
selectAllBtn.className = "select-all-btn";
selectAllBtn.onclick = (e) => {
e.stopPropagation();
selectAllInCategory(details, true);
};
const selectNoneBtn = document.createElement("button");
selectNoneBtn.textContent = "None";
selectNoneBtn.className = "select-none-btn";
selectNoneBtn.onclick = (e) => {
e.stopPropagation();
selectAllInCategory(details, false);
};
summary.appendChild(categoryName);
summary.appendChild(selectAllBtn);
summary.appendChild(selectNoneBtn);
details.appendChild(summary);
const list = document.createElement("ul");
list.className = "node-list";
(obj._nodes || []).forEach(node => {
const li = document.createElement("li");
li.dataset.nodeType = node.type; // Store node type for filtering
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "node-checkbox";
checkbox.onchange = updateSelectedCounter;
const label = document.createElement("label");
label.textContent = node.displayName;
li.appendChild(checkbox);
li.appendChild(label);
list.appendChild(li);
});
details.appendChild(list);
const subCategories = renderCategory(obj._subcategories || {}, depth + 1);
subCategories.forEach(sub => details.appendChild(sub));
return details;
});
}
function getSelectedNodeTypes() {
const selected = [];
dialogInstance.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
const nodeType = checkbox.closest('li').dataset.nodeType;
if (nodeType) selected.push(nodeType);
});
return selected;
}
function clearSelectedNodes() {
dialogInstance.querySelectorAll('.node-checkbox:checked').forEach(checkbox => {
checkbox.checked = false;
});
updateSelectedCounter();
}
function showNodeLoaderDialog() {
//Overall box //
if (dialogInstance) return;
addDialogBoxParts();
const container = document.createElement("div");
container.className = "dialog-container";
const theme = getComfyUIThemeColors();
// ESC key to close
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
closeDialog();
}
};
const closeDialog = () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.removeChild(container);
dialogInstance = null;
};
document.addEventListener('keydown', handleKeyDown);
//Title Section //
const title = document.createElement("h3");
title.textContent = "Endless 🌊✨ Node Loader";
title.className = "dialog-title";
container.appendChild(title);
title.addEventListener("mousedown", (e) => {
isDragging = true;
container.style.transform = "none"; // 🚨 Disable center alignment during drag
const rect = container.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
document.addEventListener("mousemove", handleDrag);
document.body.style.userSelect = "none";
document.addEventListener("mouseup", handleDragEnd);
e.preventDefault();
});
// Filter Section
const filterSection = document.createElement("div");
filterSection.className = "dialog-filter-section";
const filterInputContainer = document.createElement("div");
filterInputContainer.className = "filter-input-container";
const filterInput = document.createElement("input");
filterInput.type = "text";
filterInput.className = "filter-input";
filterInput.placeholder = "Filter nodes...";
const searchHistoryDropdown = document.createElement("div");
searchHistoryDropdown.className = "search-history-dropdown";
filterInput.oninput = (e) => applyFilter(e.target.value, false); // Don't save to history on input
filterInput.onkeydown = (e) => {
if (e.key === 'Enter' && e.target.value.trim()) {
addToSearchHistory(e.target.value.trim());
hideSearchHistory(searchHistoryDropdown);
} else if (e.key === 'ArrowDown' && searchHistory.length > 0) {
// Show history when user presses down arrow
showSearchHistory(filterInput);
e.preventDefault();
}
};
filterInput.onfocus = (e) => {
// Only show history if input is empty
if (!e.target.value.trim()) {
showSearchHistory(filterInput);
}
};
filterInput.onblur = (e) => {
// Save to history when leaving the field (if it has content)
if (e.target.value.trim()) {
addToSearchHistory(e.target.value.trim());
}
// Clear any pending hover timeout
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
// Delay hiding to allow clicks on dropdown items
setTimeout(() => hideSearchHistory(searchHistoryDropdown), 150);
};
filterInput.onmouseenter = () => {
// Clear auto-hide timer when mouse enters
if (searchTimeout) {
clearTimeout(searchTimeout);
searchTimeout = null;
}
// Start hover timer - show history after 1 second of hovering
hoverTimeout = setTimeout(() => {
if (searchHistory.length > 0) {
showSearchHistory(filterInput);
}
}, 1000);
};
filterInput.onmouseleave = (e) => {
// Clear hover timeout when mouse leaves
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
// Start auto-hide timer for the dropdown
searchTimeout = setTimeout(() => {
hideSearchHistory(searchHistoryDropdown);
}, 10000);
};
const expandBtn = document.createElement("button");
expandBtn.className = "filter-expand-btn";
expandBtn.textContent = "Expand All";
let isExpanded = false;
expandBtn.onclick = () => {
isExpanded = !isExpanded;
toggleAllCategories(isExpanded);
expandBtn.textContent = isExpanded ? "Collapse All" : "Expand All";
};
filterInputContainer.appendChild(filterInput);
filterInputContainer.appendChild(searchHistoryDropdown);
filterInputContainer.appendChild(expandBtn);
const nodeCounters = document.createElement("div");
nodeCounters.className = "node-counters";
nodeCounters.innerHTML = `
<span class="counter-selected">Selected: 0</span>
<span class="counter-total">Total: 0</span>
`;
filterSection.appendChild(filterInputContainer);
filterSection.appendChild(nodeCounters);
container.appendChild(filterSection);
// Recent Section //
const recentSection = document.createElement("div");
recentSection.className = "dialog-recent";
container.appendChild(recentSection);
// Node Section //
const nodeListSection = document.createElement("div");
nodeListSection.className = "dialog-list";
const nodes = Object.entries(LiteGraph.registered_node_types)
.filter(([key, value]) => key && value)
.map(([type, nodeClass]) => {
const category = nodeClass.category || "Other";
const displayName = nodeClass.title || nodeClass.name || type.split("/").pop();
return {
type,
category,
pathParts: category.split("/"),
displayName,
description: nodeClass.desc || nodeClass.description || "",
fullPath: category + "/" + displayName
};
})
.sort((a, b) => a.category.localeCompare(b.category) || a.displayName.localeCompare(b.displayName));
allNodesData = nodes; // Store for filtering
const hierarchy = buildHierarchy(nodes);
const tree = renderCategory(hierarchy);
tree.forEach(section => {
nodeListSection.appendChild(section);
});
container.appendChild(nodeListSection);
// Footer Section //
const footerSection = document.createElement("div");
footerSection.className = "dialog-footer";
const leftButtons = document.createElement("div");
leftButtons.style.display = "flex";
leftButtons.style.gap = "8px";
const spawnBtn = document.createElement("button");
spawnBtn.textContent = "Spawn Nodes";
spawnBtn.className = "dialog-btn primary";
spawnBtn.onclick = () => {
const selectedTypes = getSelectedNodeTypes();
if (selectedTypes.length === 0) {
alert("Please select at least one node to spawn.");
return;
}
spawnNodes(selectedTypes);
closeDialog(); // Use the shared closeDialog function
};
const clearBtn = document.createElement("button");
clearBtn.textContent = "Clear Selected";
clearBtn.className = "dialog-btn";
clearBtn.onclick = clearSelectedNodes;
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "Cancel";
cancelBtn.className = "dialog-btn sec";
cancelBtn.onclick = closeDialog; // Use the shared closeDialog function
leftButtons.appendChild(spawnBtn);
leftButtons.appendChild(clearBtn);
const rightButtons = document.createElement("div");
rightButtons.style.display = "flex";
rightButtons.style.gap = "8px";
const clearHistoryBtn = document.createElement("button");
clearHistoryBtn.textContent = "Clear History";
clearHistoryBtn.className = "dialog-btn";
clearHistoryBtn.onclick = () => {
searchHistory = [];
localStorage.setItem('endlessNodeLoader_searchHistory', JSON.stringify(searchHistory));
// Hide dropdown if it's currently showing
const dropdown = dialogInstance.querySelector('.search-history-dropdown');
if (dropdown) dropdown.style.display = 'none';
};
rightButtons.appendChild(clearHistoryBtn);
rightButtons.appendChild(cancelBtn);
footerSection.appendChild(leftButtons);
footerSection.appendChild(rightButtons);
container.appendChild(footerSection);
document.body.appendChild(container);
dialogInstance = container;
// Initialize
updateRecentChips();
updateSelectedCounter();
updateTotalCounter();
// Focus filter input
filterInput.focus();
}
registerEndlessTool("Node Loader", showNodeLoaderDialog);