diff --git a/js/compass_image_loader.js b/js/compass_image_loader.js index 818ede0..7877d8a 100644 --- a/js/compass_image_loader.js +++ b/js/compass_image_loader.js @@ -1,116 +1,217 @@ 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", + name: "CompassImageLoader", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData.name !== "CompassImageLoader") return; + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "CompassImageLoader") return; - const origOnNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - if (origOnNodeCreated) origOnNodeCreated.apply(this, []); + 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; + const directoryWidget = this.widgets?.find((w) => w.name === "directory"); + if (!directoryWidget) return; - directoryWidget._all_values = [...(directoryWidget.options.values || [])]; + // 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 = []; + } - try { - if (typeof this.addDOMWidget === "function") { - const inputEl = document.createElement("input"); - inputEl.type = "text"; - inputEl.value = directoryWidget.value; + // Find or create the text input for filtering + const filterWidget = this.widgets?.find((w) => w.name === "directory_filter"); + const inputEl = filterWidget?.inputEl; - inputEl.addEventListener("change", () => { - const filter = inputEl.value.toLowerCase(); - const filtered = directoryWidget._all_values.filter((v) => - v.toLowerCase().includes(filter) - ); - directoryWidget.options.values = filtered; - directoryWidget.value = filtered[0] || ""; - this.onResize?.(); - }); + 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); - const dropdownEl = document.createElement("div"); - dropdownEl.style.overflowY = "auto"; - dropdownEl.style.maxHeight = "200px"; + inputEl._prefix = directoryWidget.value || ""; + setupAutocomplete(this, directoryWidget, inputEl, listEl); - const listEl = document.createElement("ul"); - listEl.style.padding = "4px"; - listEl.style.margin = "0"; - listEl.style.background = "#333"; - - inputEl.addEventListener("focus", () => { - listEl.innerHTML = ""; - directoryWidget._all_values.forEach((v) => { - const li = document.createElement("li"); - li.textContent = v; - li.style.padding = "4px 8px"; - li.style.cursor = "pointer"; - li.addEventListener("click", () => { - inputEl.value = v; - directoryWidget.value = v; - listEl.innerHTML = ""; - }); - listEl.appendChild(li); - }); - }); - - inputEl.addEventListener("blur", (e) => { - setTimeout(() => { - if (!listEl.contains(e.relatedTarget)) { - listEl.innerHTML = ""; - } - }, 0); - }); - - const textWidget = this.addDOMWidget( - "directory_filter", - "Directory Filter", - inputEl, - {}, - { widget: directoryWidget } - ); - textWidget.computeSize = () => [1, 40]; - } - } catch (e) { - console.warn("[CompassImageLoader] Widget setup failed:", e); - } + // 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 || ""; + } }; - }, - async loadedGraphNode(node) { - if (node.type !== "CompassImageLoader") return; - - const directoryWidget = node.widgets?.find((w) => w.name === "directory"); - if (!directoryWidget) return; + // Cleanup when node is removed + const origOnRemoved = this.onRemoved; + this.onRemoved = () => { + if (origOnRemoved) origOnRemoved.apply(this, []); + listEl.remove(); + }; + } + }; + }, - setTimeout(() => { - try { - app.api.addWebSocketMessageEventListener("fresh-node-defs", (event) => { - const defs = event.detail || {}; - const compassDef = defs["CompassImageLoader"]; - if (!compassDef) return; + async loadedGraphNode(node) { + if (node.type !== "CompassImageLoader") return; - const dirs = compassDef.input?.required?.directory; - if (!dirs || !Array.isArray(dirs)) return; + const directoryWidget = node.widgets?.find((w) => w.name === "directory"); + if (!directoryWidget) return; - directoryWidget._all_values = [...dirs]; - directoryWidget.options.values = [...dirs]; + const filterWidget = node.widgets?.find((w) => w.name === "directory_filter"); + const inputEl = filterWidget?.inputEl; + const listEl = document.getElementById("compass_dir_list"); - if (!directoryWidget._all_values.includes(directoryWidget.value)) { - directoryWidget.value = directoryWidget._all_values[0] || ""; - } - }); - } catch (e) { - console.warn("[CompassImageLoader] Refresh listener failed:", e); - } - }, 100); - }, - - async getCustomWidgets() { - return null; + if (inputEl && listEl) { + setupAutocomplete(node, directoryWidget, inputEl, listEl); } + }, });