import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; function getAllNodesOfType(type) { const nodes = []; for (const node of app.graph._nodes) { if (node.type === type) nodes.push(node); } return nodes; } function parseDirectorySegments(value) { const parts = value.split("/").filter(Boolean); return { prefix: parts.length > 0 ? parts.slice(0, -1).join("/") : "", filter: parts.length > 0 ? parts[parts.length - 1] : value, isAtStart: !value.endsWith("/"), }; } function filterDirectories(allDirs, prefix, filter) { const prefixLower = prefix.toLowerCase(); const filterLower = filter.toLowerCase(); return allDirs .filter((d) => { const dParts = d.split("/").filter(Boolean); if (prefixLower) { if (dParts.length < 2) return false; const dirPrefix = dParts.slice(0, -1).join("/").toLowerCase(); if (dirPrefix !== prefixLower) return false; } else { if (dParts.length < 1) return false; } const lastPart = dParts[dParts.length - 1].toLowerCase(); return lastPart.startsWith(filterLower); }) .map((d) => { const dParts = d.split("/").filter(Boolean); return dParts[prefix ? dParts.length - 1 : dParts.length - 1]; }); } function uniqueSorted(arr) { return [...new Set(arr)].sort(); } function showDropdown(listEl, items, widget, inputEl) { listEl.innerHTML = ""; if (items.length === 0) { listEl.style.display = "none"; return; } items.forEach((item) => { const li = document.createElement("li"); li.textContent = item; li.style.padding = "4px 8px"; li.style.cursor = "pointer"; li.style.color = "#ccc"; li.style.listStyle = "none"; li.addEventListener("mouseenter", () => { document.querySelectorAll("#compass_dir_list li").forEach((el) => { el.style.background = "transparent"; }); li.style.background = "#444"; }); li.addEventListener("click", () => { const newValue = inputEl._prefix + item + "/"; inputEl.value = newValue; inputEl._prefix = newValue; widget.value = newValue; listEl.innerHTML = ""; inputEl.focus(); }); listEl.appendChild(li); }); listEl.style.display = "block"; listEl.style.background = "#222"; listEl.style.border = "1px solid #444"; listEl.style.position = "absolute"; listEl.style.zIndex = "1000"; listEl.style.width = inputEl.offsetWidth + "px"; listEl.style.maxHeight = "200px"; listEl.style.overflowY = "auto"; } function hideDropdown(listEl) { listEl.innerHTML = ""; listEl.style.display = "none"; } function setupAutocomplete(node, directoryWidget, inputEl, listEl) { inputEl._prefix = directoryWidget.value || ""; inputEl._all_values = [...(directoryWidget._all_values || [])]; inputEl.addEventListener("input", () => { const value = inputEl.value; const { prefix, filter, isAtStart } = parseDirectorySegments(value); // Determine the effective prefix: use everything up to the last slash let effectivePrefix = ""; const lastSlash = value.lastIndexOf("/"); if (lastSlash >= 0) { effectivePrefix = value.substring(0, lastSlash + 1); } const items = filterDirectories(inputEl._all_values, effectivePrefix, filter); const unique = uniqueSorted(items); showDropdown(listEl, unique, directoryWidget, inputEl); // Sync prefix back inputEl._prefix = effectivePrefix; }); inputEl.addEventListener("keydown", (e) => { if (e.key === "Escape") { hideDropdown(listEl); return; } if (e.key === "Enter" || e.key === "Tab") { const visible = listEl.querySelectorAll("li"); if (visible.length === 1) { visible[0].click(); e.preventDefault(); } else if (visible.length > 0) { // If user presses Tab with multiple options, pick the first match visible[0].click(); e.preventDefault(); } else { hideDropdown(listEl); } } }); inputEl.addEventListener("blur", () => { setTimeout(() => { if (!listEl.contains(document.activeElement)) { hideDropdown(listEl); } }, 150); }); } app.registerExtension({ name: "CompassImageLoader", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "CompassImageLoader") return; const origOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { if (origOnNodeCreated) origOnNodeCreated.apply(this, []); const directoryWidget = this.widgets?.find((w) => w.name === "directory"); if (!directoryWidget) return; // Build the full list from the node definition values const defDirs = nodeData.input?.required?.directory; if (defDirs && Array.isArray(defDirs[0])) { directoryWidget._all_values = defDirs[0]; } else { directoryWidget._all_values = []; } // Find or create the text input for filtering const filterWidget = this.widgets?.find((w) => w.name === "directory_filter"); const inputEl = filterWidget?.inputEl; if (inputEl) { // Create dropdown list element const listEl = document.createElement("ul"); listEl.id = "compass_dir_list"; listEl.style.display = "none"; listEl.style.position = "absolute"; listEl.style.zIndex = "9999"; document.body.appendChild(listEl); inputEl._prefix = directoryWidget.value || ""; setupAutocomplete(this, directoryWidget, inputEl, listEl); // When the original combo value changes from workflow load, sync const origSetValue = directoryWidget.callback ? directoryWidget.callback : () => {}; directoryWidget.callback = (value) => { origSetValue.call(directoryWidget, value); if (inputEl) { inputEl._prefix = value || ""; inputEl.value = value || ""; } }; // Cleanup when node is removed const origOnRemoved = this.onRemoved; this.onRemoved = () => { if (origOnRemoved) origOnRemoved.apply(this, []); listEl.remove(); }; } }; }, async loadedGraphNode(node) { if (node.type !== "CompassImageLoader") return; const directoryWidget = node.widgets?.find((w) => w.name === "directory"); if (!directoryWidget) return; const filterWidget = node.widgets?.find((w) => w.name === "directory_filter"); const inputEl = filterWidget?.inputEl; const listEl = document.getElementById("compass_dir_list"); if (inputEl && listEl) { setupAutocomplete(node, directoryWidget, inputEl, listEl); } }, });