Spaces:
Building
on
A10G
Building
on
A10G
import {app} from "../../scripts/app.js"; | |
// Adds filtering to combo context menus | |
const ext = { | |
name: "Comfy.ContextMenuFilter", | |
init() { | |
const ctxMenu = LiteGraph.ContextMenu; | |
LiteGraph.ContextMenu = function (values, options) { | |
const ctx = ctxMenu.call(this, values, options); | |
// If we are a dark menu (only used for combo boxes) then add a filter input | |
if (options?.className === "dark" && values?.length > 10) { | |
const filter = document.createElement("input"); | |
filter.classList.add("comfy-context-menu-filter"); | |
filter.placeholder = "Filter list"; | |
this.root.prepend(filter); | |
const items = Array.from(this.root.querySelectorAll(".litemenu-entry")); | |
let displayedItems = [...items]; | |
let itemCount = displayedItems.length; | |
// We must request an animation frame for the current node of the active canvas to update. | |
requestAnimationFrame(() => { | |
const currentNode = LGraphCanvas.active_canvas.current_node; | |
const clickedComboValue = currentNode.widgets | |
?.filter(w => w.type === "combo" && w.options.values.length === values.length) | |
.find(w => w.options.values.every((v, i) => v === values[i])) | |
?.value; | |
let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0; | |
if (selectedIndex < 0) { | |
selectedIndex = 0; | |
} | |
let selectedItem = displayedItems[selectedIndex]; | |
updateSelected(); | |
// Apply highlighting to the selected item | |
function updateSelected() { | |
selectedItem?.style.setProperty("background-color", ""); | |
selectedItem?.style.setProperty("color", ""); | |
selectedItem = displayedItems[selectedIndex]; | |
selectedItem?.style.setProperty("background-color", "#ccc", "important"); | |
selectedItem?.style.setProperty("color", "#000", "important"); | |
} | |
const positionList = () => { | |
const rect = this.root.getBoundingClientRect(); | |
// If the top is off-screen then shift the element with scaling applied | |
if (rect.top < 0) { | |
const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight; | |
const shift = (this.root.clientHeight * scale) / 2; | |
this.root.style.top = -shift + "px"; | |
} | |
} | |
// Arrow up/down to select items | |
filter.addEventListener("keydown", (event) => { | |
switch (event.key) { | |
case "ArrowUp": | |
event.preventDefault(); | |
if (selectedIndex === 0) { | |
selectedIndex = itemCount - 1; | |
} else { | |
selectedIndex--; | |
} | |
updateSelected(); | |
break; | |
case "ArrowRight": | |
event.preventDefault(); | |
selectedIndex = itemCount - 1; | |
updateSelected(); | |
break; | |
case "ArrowDown": | |
event.preventDefault(); | |
if (selectedIndex === itemCount - 1) { | |
selectedIndex = 0; | |
} else { | |
selectedIndex++; | |
} | |
updateSelected(); | |
break; | |
case "ArrowLeft": | |
event.preventDefault(); | |
selectedIndex = 0; | |
updateSelected(); | |
break; | |
case "Enter": | |
selectedItem?.click(); | |
break; | |
case "Escape": | |
this.close(); | |
break; | |
} | |
}); | |
filter.addEventListener("input", () => { | |
// Hide all items that don't match our filter | |
const term = filter.value.toLocaleLowerCase(); | |
// When filtering, recompute which items are visible for arrow up/down and maintain selection. | |
displayedItems = items.filter(item => { | |
const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term); | |
item.style.display = isVisible ? "block" : "none"; | |
return isVisible; | |
}); | |
selectedIndex = 0; | |
if (displayedItems.includes(selectedItem)) { | |
selectedIndex = displayedItems.findIndex(d => d === selectedItem); | |
} | |
itemCount = displayedItems.length; | |
updateSelected(); | |
// If we have an event then we can try and position the list under the source | |
if (options.event) { | |
let top = options.event.clientY - 10; | |
const bodyRect = document.body.getBoundingClientRect(); | |
const rootRect = this.root.getBoundingClientRect(); | |
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) { | |
top = Math.max(0, bodyRect.height - rootRect.height - 10); | |
} | |
this.root.style.top = top + "px"; | |
positionList(); | |
} | |
}); | |
requestAnimationFrame(() => { | |
// Focus the filter box when opening | |
filter.focus(); | |
positionList(); | |
}); | |
}) | |
} | |
return ctx; | |
}; | |
LiteGraph.ContextMenu.prototype = ctxMenu.prototype; | |
}, | |
} | |
app.registerExtension(ext); | |