mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-04-19 10:53:29 +00:00
Improve ContextMenuFilter extension
This commit is contained in:
parent
29c50954ea
commit
bfebe2d6c3
@ -1,132 +1,138 @@
|
|||||||
import { app } from "/scripts/app.js";
|
import {app} from "/scripts/app.js";
|
||||||
|
|
||||||
// Adds filtering to combo context menus
|
// Adds filtering to combo context menus
|
||||||
|
|
||||||
const id = "Comfy.ContextMenuFilter";
|
const ext = {
|
||||||
app.registerExtension({
|
name: "Comfy.ContextMenuFilter",
|
||||||
name: id,
|
|
||||||
init() {
|
init() {
|
||||||
const ctxMenu = LiteGraph.ContextMenu;
|
const ctxMenu = LiteGraph.ContextMenu;
|
||||||
|
|
||||||
LiteGraph.ContextMenu = function (values, options) {
|
LiteGraph.ContextMenu = function (values, options) {
|
||||||
const ctx = ctxMenu.call(this, 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 we are a dark menu (only used for combo boxes) then add a filter input
|
||||||
if (options?.className === "dark" && values?.length > 10) {
|
if (options?.className === "dark" && values?.length > 10) {
|
||||||
const filter = document.createElement("input");
|
const filter = document.createElement("input");
|
||||||
Object.assign(filter.style, {
|
filter.classList.add("comfy-context-menu-filter");
|
||||||
width: "calc(100% - 10px)",
|
|
||||||
border: "0",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
background: "#333",
|
|
||||||
border: "1px solid #999",
|
|
||||||
margin: "0 0 5px 5px",
|
|
||||||
color: "#fff",
|
|
||||||
});
|
|
||||||
filter.placeholder = "Filter list";
|
filter.placeholder = "Filter list";
|
||||||
this.root.prepend(filter);
|
this.root.prepend(filter);
|
||||||
|
|
||||||
let selectedIndex = 0;
|
const items = Array.from(this.root.querySelectorAll(".litemenu-entry"));
|
||||||
let items = this.root.querySelectorAll(".litemenu-entry");
|
let displayedItems = [...items];
|
||||||
let itemCount = items.length;
|
let itemCount = displayedItems.length;
|
||||||
let selectedItem;
|
|
||||||
|
|
||||||
// Apply highlighting to the selected item
|
// We must request an animation frame for the current node of the active canvas to update.
|
||||||
function updateSelected() {
|
requestAnimationFrame(() => {
|
||||||
if (selectedItem) {
|
const currentNode = LGraphCanvas.active_canvas.current_node;
|
||||||
selectedItem.style.setProperty("background-color", "");
|
const clickedComboValue = currentNode.widgets
|
||||||
selectedItem.style.setProperty("color", "");
|
.filter(w => w.type === "combo" && w.options.values.length === values.length)
|
||||||
}
|
.find(w => w.options.values.every((v, i) => v === values[i]))
|
||||||
selectedItem = items[selectedIndex];
|
.value;
|
||||||
if (selectedItem) {
|
|
||||||
selectedItem.style.setProperty("background-color", "#ccc", "important");
|
|
||||||
selectedItem.style.setProperty("color", "#000", "important");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionList = () => {
|
let selectedIndex = values.findIndex(v => v === clickedComboValue);
|
||||||
const rect = this.root.getBoundingClientRect();
|
let selectedItem = displayedItems?.[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();
|
|
||||||
|
|
||||||
// Arrow up/down to select items
|
|
||||||
filter.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
selectedIndex = itemCount - 1;
|
|
||||||
} else {
|
|
||||||
selectedIndex--;
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
selectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = visibleItems;
|
|
||||||
updateSelected();
|
updateSelected();
|
||||||
|
|
||||||
// If we have an event then we can try and position the list under the source
|
// Apply highlighting to the selected item
|
||||||
if (options.event) {
|
function updateSelected() {
|
||||||
let top = options.event.clientY - 10;
|
selectedItem?.style.setProperty("background-color", "");
|
||||||
|
selectedItem?.style.setProperty("color", "");
|
||||||
const bodyRect = document.body.getBoundingClientRect();
|
selectedItem = displayedItems[selectedIndex];
|
||||||
const rootRect = this.root.getBoundingClientRect();
|
selectedItem?.style.setProperty("background-color", "#ccc", "important");
|
||||||
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
|
selectedItem?.style.setProperty("color", "#000", "important");
|
||||||
top = Math.max(0, bodyRect.height - rootRect.height - 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.root.style.top = top + "px";
|
|
||||||
positionList();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
const positionList = () => {
|
||||||
// Focus the filter box when opening
|
const rect = this.root.getBoundingClientRect();
|
||||||
filter.focus();
|
|
||||||
|
|
||||||
positionList();
|
// 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;
|
return ctx;
|
||||||
@ -134,4 +140,6 @@ app.registerExtension({
|
|||||||
|
|
||||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
|
||||||
|
app.registerExtension(ext);
|
||||||
|
@ -50,7 +50,7 @@ body {
|
|||||||
padding: 30px 30px 10px 30px;
|
padding: 30px 30px 10px 30px;
|
||||||
background-color: var(--comfy-menu-bg); /* Modal background */
|
background-color: var(--comfy-menu-bg); /* Modal background */
|
||||||
color: var(--error-text);
|
color: var(--error-text);
|
||||||
box-shadow: 0px 0px 20px #888888;
|
box-shadow: 0 0 20px #888888;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -84,7 +84,7 @@ body {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 0%;
|
right: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
@ -252,7 +252,7 @@ button.comfy-queue-btn {
|
|||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
left: auto !important;
|
left: auto !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.comfy-menu span.drag-handle {
|
.comfy-menu span.drag-handle {
|
||||||
visibility:hidden
|
visibility:hidden
|
||||||
@ -291,7 +291,7 @@ button.comfy-queue-btn {
|
|||||||
|
|
||||||
.litegraph .dialog {
|
.litegraph .dialog {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
font-family: Arial;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.litegraph .litemenu-entry.has_submenu {
|
.litegraph .litemenu-entry.has_submenu {
|
||||||
@ -330,6 +330,13 @@ button.comfy-queue-btn {
|
|||||||
color: var(--input-text) !important;
|
color: var(--input-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comfy-context-menu-filter {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #999;
|
||||||
|
margin: 0 0 5px 5px;
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Search box */
|
/* Search box */
|
||||||
|
|
||||||
.litegraph.litesearchbox {
|
.litegraph.litesearchbox {
|
||||||
|
Loading…
Reference in New Issue
Block a user