|
|
|
@ -1,132 +1,138 @@
|
|
|
|
|
import { app } from "/scripts/app.js"; |
|
|
|
|
import {app} from "/scripts/app.js"; |
|
|
|
|
|
|
|
|
|
// Adds filtering to combo context menus
|
|
|
|
|
|
|
|
|
|
const id = "Comfy.ContextMenuFilter"; |
|
|
|
|
app.registerExtension({ |
|
|
|
|
name: id, |
|
|
|
|
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"); |
|
|
|
|
Object.assign(filter.style, { |
|
|
|
|
width: "calc(100% - 10px)", |
|
|
|
|
border: "0", |
|
|
|
|
boxSizing: "border-box", |
|
|
|
|
background: "#333", |
|
|
|
|
border: "1px solid #999", |
|
|
|
|
margin: "0 0 5px 5px", |
|
|
|
|
color: "#fff", |
|
|
|
|
}); |
|
|
|
|
filter.classList.add("comfy-context-menu-filter"); |
|
|
|
|
filter.placeholder = "Filter list"; |
|
|
|
|
this.root.prepend(filter); |
|
|
|
|
|
|
|
|
|
let selectedIndex = 0; |
|
|
|
|
let items = this.root.querySelectorAll(".litemenu-entry"); |
|
|
|
|
let itemCount = items.length; |
|
|
|
|
let selectedItem; |
|
|
|
|
|
|
|
|
|
// Apply highlighting to the selected item
|
|
|
|
|
function updateSelected() { |
|
|
|
|
if (selectedItem) { |
|
|
|
|
selectedItem.style.setProperty("background-color", ""); |
|
|
|
|
selectedItem.style.setProperty("color", ""); |
|
|
|
|
} |
|
|
|
|
selectedItem = items[selectedIndex]; |
|
|
|
|
if (selectedItem) { |
|
|
|
|
selectedItem.style.setProperty("background-color", "#ccc", "important"); |
|
|
|
|
selectedItem.style.setProperty("color", "#000", "important"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
const items = Array.from(this.root.querySelectorAll(".litemenu-entry")); |
|
|
|
|
let displayedItems = [...items]; |
|
|
|
|
let itemCount = displayedItems.length; |
|
|
|
|
|
|
|
|
|
const positionList = () => { |
|
|
|
|
const rect = this.root.getBoundingClientRect(); |
|
|
|
|
// 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 = values.findIndex(v => v === clickedComboValue); |
|
|
|
|
let selectedItem = displayedItems?.[selectedIndex]; |
|
|
|
|
updateSelected(); |
|
|
|
|
|
|
|
|
|
// 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"; |
|
|
|
|
// 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"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateSelected(); |
|
|
|
|
const positionList = () => { |
|
|
|
|
const rect = this.root.getBoundingClientRect(); |
|
|
|
|
|
|
|
|
|
// Arrow up/down to select items
|
|
|
|
|
filter.addEventListener("keydown", (e) => { |
|
|
|
|
if (e.key === "ArrowUp") { |
|
|
|
|
if (selectedIndex === 0) { |
|
|
|
|
selectedIndex = itemCount - 1; |
|
|
|
|
} else { |
|
|
|
|
selectedIndex--; |
|
|
|
|
// 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"; |
|
|
|
|
} |
|
|
|
|
updateSelected(); |
|
|
|
|
e.preventDefault(); |
|
|
|
|
} else if (e.key === "ArrowDown") { |
|
|
|
|
if (selectedIndex === itemCount - 1) { |
|
|
|
|
selectedIndex = 0; |
|
|
|
|
} else { |
|
|
|
|
selectedIndex++; |
|
|
|
|
} |
|
|
|
|
updateSelected(); |
|
|
|
|
e.preventDefault(); |
|
|
|
|
} else if ((selectedItem && e.key === "Enter") || e.keyCode === 13 || e.keyCode === 10) { |
|
|
|
|
selectedItem.click(); |
|
|
|
|
} else if(e.key === "Escape") { |
|
|
|
|
this.close(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
filter.addEventListener("input", () => { |
|
|
|
|
// Hide all items that dont match our filter
|
|
|
|
|
const term = filter.value.toLocaleLowerCase(); |
|
|
|
|
items = this.root.querySelectorAll(".litemenu-entry"); |
|
|
|
|
// When filtering recompute which items are visible for arrow up/down
|
|
|
|
|
// Try and maintain selection
|
|
|
|
|
let visibleItems = []; |
|
|
|
|
for (const item of items) { |
|
|
|
|
const visible = !term || item.textContent.toLocaleLowerCase().includes(term); |
|
|
|
|
if (visible) { |
|
|
|
|
item.style.display = "block"; |
|
|
|
|
if (item === selectedItem) { |
|
|
|
|
selectedIndex = visibleItems.length; |
|
|
|
|
} |
|
|
|
|
visibleItems.push(item); |
|
|
|
|
} else { |
|
|
|
|
item.style.display = "none"; |
|
|
|
|
if (item === selectedItem) { |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
items = visibleItems; |
|
|
|
|
updateSelected(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
updateSelected(); |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
// 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; |
|
|
|
|
|
|
|
|
|
this.root.style.top = top + "px"; |
|
|
|
|
positionList(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => { |
|
|
|
|
// Focus the filter box when opening
|
|
|
|
|
filter.focus(); |
|
|
|
|
this.root.style.top = top + "px"; |
|
|
|
|
positionList(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
positionList(); |
|
|
|
|
}); |
|
|
|
|
requestAnimationFrame(() => { |
|
|
|
|
// Focus the filter box when opening
|
|
|
|
|
filter.focus(); |
|
|
|
|
|
|
|
|
|
positionList(); |
|
|
|
|
}); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ctx; |
|
|
|
@ -134,4 +140,6 @@ app.registerExtension({
|
|
|
|
|
|
|
|
|
|
LiteGraph.ContextMenu.prototype = ctxMenu.prototype; |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
app.registerExtension(ext); |
|
|
|
|