Fix hierarchical autocomplete: select directory segments, not files; stop when direction subdir is found
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
function getAllNodesOfType(type) {
|
||||
const nodes = [];
|
||||
@@ -9,49 +8,32 @@ function getAllNodesOfType(type) {
|
||||
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 filterChildDirs(allDirs, parentPrefix) {
|
||||
const prefixNorm = parentPrefix.replace(/\/$/, "").toLowerCase();
|
||||
const results = [];
|
||||
|
||||
for (const dir of allDirs) {
|
||||
const parts = dir.split("/").filter(Boolean);
|
||||
if (parts.length < 1) continue;
|
||||
|
||||
const parent = parts.slice(0, -1).join("/").toLowerCase();
|
||||
const child = parts[parts.length - 1];
|
||||
|
||||
if (parent === prefixNorm) {
|
||||
results.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(results)].sort();
|
||||
}
|
||||
|
||||
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) {
|
||||
function showDropdown(listEl, items, directoryWidget, inputEl) {
|
||||
listEl.innerHTML = "";
|
||||
if (items.length === 0) {
|
||||
listEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
items.forEach((item) => {
|
||||
for (const item of items) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = item;
|
||||
li.style.padding = "4px 8px";
|
||||
@@ -59,29 +41,30 @@ function showDropdown(listEl, items, widget, inputEl) {
|
||||
li.style.color = "#ccc";
|
||||
li.style.listStyle = "none";
|
||||
li.addEventListener("mouseenter", () => {
|
||||
document.querySelectorAll("#compass_dir_list li").forEach((el) => {
|
||||
el.style.background = "transparent";
|
||||
});
|
||||
listEl.querySelectorAll("li").forEach((el) => (el.style.background = "transparent"));
|
||||
li.style.background = "#444";
|
||||
});
|
||||
li.addEventListener("click", () => {
|
||||
const newValue = inputEl._prefix + item + "/";
|
||||
const newValue = inputEl._currentPrefix + item + "/";
|
||||
inputEl.value = newValue;
|
||||
inputEl._prefix = newValue;
|
||||
widget.value = newValue;
|
||||
listEl.innerHTML = "";
|
||||
inputEl._currentPrefix = newValue;
|
||||
directoryWidget.value = newValue;
|
||||
hideDropdown(listEl);
|
||||
inputEl.focus();
|
||||
});
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
|
||||
Object.assign(listEl.style, {
|
||||
display: "block",
|
||||
background: "#222",
|
||||
border: "1px solid #444",
|
||||
position: "absolute",
|
||||
zIndex: "9999",
|
||||
width: inputEl.offsetWidth + "px",
|
||||
maxHeight: "200px",
|
||||
overflowY: "auto",
|
||||
});
|
||||
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) {
|
||||
@@ -89,58 +72,6 @@ function hideDropdown(listEl) {
|
||||
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",
|
||||
|
||||
@@ -154,49 +85,112 @@ app.registerExtension({
|
||||
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 = [];
|
||||
}
|
||||
directoryWidget._all_values = defDirs && Array.isArray(defDirs[0]) ? defDirs[0] : [];
|
||||
|
||||
// Find or create the text input for filtering
|
||||
const filterWidget = this.widgets?.find((w) => w.name === "directory_filter");
|
||||
const inputEl = filterWidget?.inputEl;
|
||||
if (!inputEl) return;
|
||||
|
||||
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 listEl = document.createElement("ul");
|
||||
listEl.style.display = "none";
|
||||
document.body.appendChild(listEl);
|
||||
|
||||
inputEl._prefix = directoryWidget.value || "";
|
||||
setupAutocomplete(this, directoryWidget, inputEl, listEl);
|
||||
inputEl._currentPrefix = directoryWidget.value || "";
|
||||
|
||||
// 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 || "";
|
||||
inputEl.addEventListener("input", () => {
|
||||
const val = inputEl.value;
|
||||
const lastSlash = val.lastIndexOf("/");
|
||||
|
||||
let prefix, filter;
|
||||
if (lastSlash >= 0) {
|
||||
prefix = val.substring(0, lastSlash + 1);
|
||||
filter = val.substring(lastSlash + 1);
|
||||
} else {
|
||||
prefix = "";
|
||||
filter = val;
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
const cleanPrefix = prefix.replace(/\/$/, "");
|
||||
const candidates = filterChildDirs(directoryWidget._all_values, cleanPrefix);
|
||||
const filtered = filter
|
||||
? candidates.filter((c) => c.toLowerCase().startsWith(filter.toLowerCase()))
|
||||
: candidates;
|
||||
showDropdown(listEl, filtered, directoryWidget, inputEl);
|
||||
} else {
|
||||
const filtered = filter
|
||||
? directoryWidget._all_values.filter((d) =>
|
||||
d.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
: [...new Set(directoryWidget._all_values.map((d) => d.split("/")[0]))].sort();
|
||||
showDropdown(listEl, filtered, directoryWidget, inputEl);
|
||||
}
|
||||
});
|
||||
|
||||
inputEl.addEventListener("focus", () => {
|
||||
const val = inputEl.value;
|
||||
const lastSlash = val.lastIndexOf("/");
|
||||
|
||||
if (lastSlash >= 0 && val.endsWith("/")) {
|
||||
const prefix = val.replace(/\/$/, "");
|
||||
inputEl._currentPrefix = prefix + "/";
|
||||
const items = filterChildDirs(directoryWidget._all_values, prefix);
|
||||
showDropdown(listEl, items, directoryWidget, inputEl);
|
||||
} else if (lastSlash >= 0) {
|
||||
const prefix = val.substring(0, lastSlash + 1);
|
||||
inputEl._currentPrefix = prefix;
|
||||
inputEl.focus();
|
||||
return;
|
||||
} else {
|
||||
inputEl._currentPrefix = "";
|
||||
const items = filter
|
||||
? directoryWidget._all_values.filter((d) =>
|
||||
d.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
: [...new Set(directoryWidget._all_values.map((d) => d.split("/")[0]))].sort();
|
||||
showDropdown(listEl, items, directoryWidget, inputEl);
|
||||
}
|
||||
});
|
||||
|
||||
inputEl.addEventListener("blur", () => {
|
||||
setTimeout(() => {
|
||||
if (!listEl.contains(document.activeElement)) {
|
||||
hideDropdown(listEl);
|
||||
}
|
||||
};
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Cleanup when node is removed
|
||||
const origOnRemoved = this.onRemoved;
|
||||
this.onRemoved = () => {
|
||||
if (origOnRemoved) origOnRemoved.apply(this, []);
|
||||
listEl.remove();
|
||||
};
|
||||
}
|
||||
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) {
|
||||
visible[0].click();
|
||||
e.preventDefault();
|
||||
}
|
||||
hideDropdown(listEl);
|
||||
}
|
||||
});
|
||||
|
||||
const origSetValue = directoryWidget.callback ? directoryWidget.callback.bind(directoryWidget) : () => {};
|
||||
directoryWidget.callback = (value) => {
|
||||
origSetValue(value);
|
||||
inputEl._currentPrefix = value || "";
|
||||
inputEl.value = value || "";
|
||||
};
|
||||
|
||||
const origOnRemoved = this.onRemoved ? this.onRemoved.bind(this) : () => {};
|
||||
this.onRemoved = () => {
|
||||
origOnRemoved();
|
||||
listEl.remove();
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
@@ -204,14 +198,12 @@ app.registerExtension({
|
||||
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);
|
||||
inputEl._currentPrefix = directoryWidget?.value || "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user