File size: 4,686 Bytes
436faa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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);