feat: enable node list display and selection in the pop-out viewer and update version.

This commit is contained in:
AEmotionStudio
2026-01-05 16:22:52 -08:00
parent ac8bda31ac
commit e2e19f83e1
8 changed files with 313 additions and 10 deletions
+4
View File
@@ -63,6 +63,10 @@ export class InfoPanel {
this.magnifyGlass.popOutManager.onStateChange = (isOpen: boolean) => {
this.uiManager.updateControlStates();
};
// Listen for node selection from popout
this.magnifyGlass.popOutManager.onNodeSelect = (nodeId: number) => {
this.onNodeSelected(nodeId);
};
}
// Set up node selection callback
+68 -3
View File
@@ -12,11 +12,19 @@ import { Logger } from '../shared/logger';
* Message types for BroadcastChannel communication
*/
interface PopOutMessage {
type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong';
data?: string | Partial<PopOutConfig> | PopOutInfo;
type: 'frame' | 'config' | 'info' | 'close' | 'ping' | 'pong' | 'node-select' | 'nodes-list' | 'request-nodes';
data?: string | Partial<PopOutConfig> | PopOutInfo | NodeListData | number;
timestamp?: number;
}
/**
* Node list data sent to popout for dropdown
*/
interface NodeListData {
nodes: { id: number; title: string; type: string; order?: number }[];
sortBy: 'title' | 'execOrder';
}
interface PopOutConfig {
glassSize: number;
borderColor: string;
@@ -68,6 +76,7 @@ export class PopOutManager {
private isOpen: boolean = false;
private popOutWindow: Window | null = null;
public onStateChange: ((isOpen: boolean) => void) | null = null;
public onNodeSelect: ((nodeId: number) => void) | null = null;
private lastPongTime: number = 0;
private pingInterval: number | null = null;
private viewerUrl: string;
@@ -92,7 +101,7 @@ export class PopOutManager {
*/
private getViewerUrl(): string {
// Cache-busting version - increment to force refresh
const version = 'v20';
const version = 'v21';
// Find the extension's base URL from the loaded scripts
const scripts = document.querySelectorAll('script[src*="magnify"]');
@@ -168,6 +177,17 @@ export class PopOutManager {
Logger.debug('[PopOut] Viewer tab closed');
if (this.onStateChange) this.onStateChange(false);
break;
case 'request-nodes':
// Popout is requesting the node list for dropdown
Logger.debug('[PopOut] Received request-nodes:', message.data);
this.handleNodeListRequest(message.data as 'title' | 'execOrder');
break;
case 'node-select':
// Popout selected a node, notify main window
if (this.onNodeSelect && typeof message.data === 'number') {
this.onNodeSelect(message.data);
}
break;
}
}
@@ -316,6 +336,51 @@ export class PopOutManager {
});
}
/**
* Handle node list request from popout viewer.
* Fetches nodes from canvas and sends them to the popout.
*/
private handleNodeListRequest(sortBy: 'title' | 'execOrder'): void {
if (!this.channel) return;
try {
// Access nodes from ComfyUI's app object
const app = (window as any).app;
const nodes = app?.graph?._nodes || [];
let nodeList: { id: number; title: string; type: string; order?: number }[];
if (sortBy === 'execOrder') {
nodeList = nodes
.map((n: any) => ({
id: n.id,
title: n.title || 'Untitled',
type: n.type || 'Unknown',
order: n.order ?? -1
}))
.filter((n: any) => n.order >= 0)
.sort((a: any, b: any) => a.order - b.order);
} else {
nodeList = nodes
.map((n: any) => ({
id: n.id,
title: n.title || 'Untitled',
type: n.type || 'Unknown'
}))
.sort((a: any, b: any) => a.title.localeCompare(b.title));
}
this.sendMessage({
type: 'nodes-list',
data: { nodes: nodeList, sortBy }
});
Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);
} catch (e) {
Logger.error('[PopOut] Failed to get node list:', e);
}
}
/**
* Sanitize info object for BroadcastChannel transfer.
* Removes functions, circular references, and non-serializable data.
+1
View File
@@ -214,6 +214,7 @@ export interface MagnifyGlassInstance {
sendInfo(info: unknown): void;
toggle(): void;
onStateChange?: ((isOpen: boolean) => void) | null;
onNodeSelect?: ((nodeId: number) => void) | null;
updateTheme?(theme: string): void;
};
lastKnownMousePosition: { x: number; y: number };
+3
View File
@@ -44,6 +44,9 @@ class InfoPanel {
this.magnifyGlass.popOutManager.onStateChange = (isOpen) => {
this.uiManager.updateControlStates();
};
this.magnifyGlass.popOutManager.onNodeSelect = (nodeId) => {
this.onNodeSelected(nodeId);
};
}
this.uiManager.onNodeSelected = (nodeId) => {
this.onNodeSelected(nodeId);
File diff suppressed because one or more lines are too long
+45 -1
View File
@@ -9,6 +9,7 @@ class PopOutManager {
__publicField(this, "isOpen", false);
__publicField(this, "popOutWindow", null);
__publicField(this, "onStateChange", null);
__publicField(this, "onNodeSelect", null);
__publicField(this, "lastPongTime", 0);
__publicField(this, "pingInterval", null);
__publicField(this, "viewerUrl");
@@ -27,7 +28,7 @@ class PopOutManager {
* Get the URL for the pop-out viewer page.
*/
getViewerUrl() {
const version = "v20";
const version = "v21";
const scripts = document.querySelectorAll('script[src*="magnify"]');
Logger.debug(`[PopOut] Found ${scripts.length} magnify scripts`);
if (scripts.length > 0) {
@@ -87,6 +88,15 @@ class PopOutManager {
Logger.debug("[PopOut] Viewer tab closed");
if (this.onStateChange) this.onStateChange(false);
break;
case "request-nodes":
Logger.debug("[PopOut] Received request-nodes:", message.data);
this.handleNodeListRequest(message.data);
break;
case "node-select":
if (this.onNodeSelect && typeof message.data === "number") {
this.onNodeSelect(message.data);
}
break;
}
}
/**
@@ -201,6 +211,40 @@ class PopOutManager {
data: sanitizedInfo || void 0
});
}
/**
* Handle node list request from popout viewer.
* Fetches nodes from canvas and sends them to the popout.
*/
handleNodeListRequest(sortBy) {
var _a;
if (!this.channel) return;
try {
const app = window.app;
const nodes = ((_a = app == null ? void 0 : app.graph) == null ? void 0 : _a._nodes) || [];
let nodeList;
if (sortBy === "execOrder") {
nodeList = nodes.map((n) => ({
id: n.id,
title: n.title || "Untitled",
type: n.type || "Unknown",
order: n.order ?? -1
})).filter((n) => n.order >= 0).sort((a, b) => a.order - b.order);
} else {
nodeList = nodes.map((n) => ({
id: n.id,
title: n.title || "Untitled",
type: n.type || "Unknown"
})).sort((a, b) => a.title.localeCompare(b.title));
}
this.sendMessage({
type: "nodes-list",
data: { nodes: nodeList, sortBy }
});
Logger.debug(`[PopOut] Sent ${nodeList.length} nodes to viewer (sorted by ${sortBy})`);
} catch (e) {
Logger.error("[PopOut] Failed to get node list:", e);
}
}
/**
* Sanitize info object for BroadcastChannel transfer.
* Removes functions, circular references, and non-serializable data.
File diff suppressed because one or more lines are too long
+190 -4
View File
@@ -730,6 +730,72 @@
box-shadow: none !important;
object-fit: cover;
}
/* Clickable row styles */
.info-row.clickable-row {
cursor: pointer;
transition: background 0.2s ease;
}
.info-row.clickable-row:hover {
background: var(--bg-elevated);
}
.dropdown-indicator {
margin-left: 4px;
opacity: 0.6;
font-size: 10px;
}
/* Node selector dropdown */
.node-selector-dropdown {
position: absolute;
z-index: 100000;
max-height: 300px;
overflow-y: auto;
background: var(--bg-elevated);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 200px;
max-width: 300px;
}
.node-selector-dropdown .dropdown-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.node-selector-dropdown .dropdown-item:hover {
background: var(--bg-tertiary);
}
.node-selector-dropdown .dropdown-item:last-child {
border-bottom: none;
}
.node-selector-dropdown .node-order {
color: var(--accent-cyan);
font-weight: 600;
min-width: 24px;
}
.node-selector-dropdown .node-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-selector-dropdown .node-type {
color: var(--text-tertiary);
font-size: 11px;
}
</style>
</head>
@@ -904,6 +970,9 @@
case 'close':
markDisconnected();
break;
case 'nodes-list':
handleNodesList(message.data);
break;
}
};
@@ -1009,14 +1078,14 @@
<span class="section-badge">${escapeHtml(info.hoveredNode.type || 'Unknown')}</span>
</div>
<div class="section-body">
<div class="info-row">
<div class="info-row clickable-row" onclick="requestNodeDropdown('title', this)">
<span class="info-label">Title</span>
<span class="info-value">${escapeHtml(info.hoveredNode.title || 'Untitled')}</span>
<span class="info-value">${escapeHtml(info.hoveredNode.title || 'Untitled')}<span class="dropdown-indicator">▼</span></span>
</div>
${info.hoveredNode.executionOrder !== undefined ? `
<div class="info-row">
<div class="info-row clickable-row" onclick="requestNodeDropdown('execOrder', this)">
<span class="info-label">Exec Order</span>
<span class="info-value">${info.hoveredNode.executionOrder}</span>
<span class="info-value">${info.hoveredNode.executionOrder}<span class="dropdown-indicator">▼</span></span>
</div>` : ''}
${info.hoveredNode.category ? `
<div class="info-row">
@@ -1160,6 +1229,123 @@
if (existing) existing.remove();
}
// Node selection dropdown functionality
let currentDropdownAnchorRect = null;
let pendingDropdownType = null;
function requestNodeDropdown(type, anchorElement) {
console.log('[Popout] requestNodeDropdown called:', type, anchorElement);
// Store anchor position immediately (before potential re-render)
if (anchorElement) {
currentDropdownAnchorRect = anchorElement.getBoundingClientRect();
console.log('[Popout] Stored anchorRect:', currentDropdownAnchorRect);
}
pendingDropdownType = type;
// Request node list from main window
if (channel) {
console.log('[Popout] Sending request-nodes message');
channel.postMessage({ type: 'request-nodes', data: type });
} else {
console.log('[Popout] ERROR: channel is null!');
}
}
function handleNodesList(data) {
console.log('[Popout] handleNodesList called:', data);
console.log('[Popout] currentDropdownAnchorRect:', currentDropdownAnchorRect);
if (!data || !data.nodes || !currentDropdownAnchorRect) {
console.log('[Popout] handleNodesList returning early');
return;
}
showDropdown(data.nodes, data.sortBy);
}
function showDropdown(nodes, sortBy) {
// Save anchorRect before hideDropdown clears it
const savedAnchorRect = currentDropdownAnchorRect;
hideDropdown();
if (nodes.length === 0) return;
if (!savedAnchorRect) return;
const dropdown = document.createElement('div');
dropdown.className = 'node-selector-dropdown';
dropdown.id = 'node-dropdown';
nodes.forEach(node => {
const item = document.createElement('div');
item.className = 'dropdown-item';
if (sortBy === 'execOrder' && node.order !== undefined) {
item.innerHTML = `
<span class="node-order">#${node.order}</span>
<span class="node-title">${escapeHtml(node.title)}</span>
<span class="node-type">${escapeHtml(node.type)}</span>
`;
} else {
item.innerHTML = `
<span class="node-title">${escapeHtml(node.title)}</span>
<span class="node-type">${escapeHtml(node.type)}</span>
`;
}
item.addEventListener('click', (e) => {
e.stopPropagation();
selectNode(node.id);
hideDropdown();
});
dropdown.appendChild(item);
});
// Position dropdown
document.body.appendChild(dropdown);
console.log('[Popout] showDropdown: Created dropdown with', nodes.length, 'nodes');
const anchorRect = savedAnchorRect;
dropdown.style.top = `${anchorRect.bottom + 4}px`;
dropdown.style.left = `${anchorRect.left}px`;
console.log('[Popout] showDropdown: Positioned at', dropdown.style.top, dropdown.style.left);
// Adjust if off-screen
const dropdownRect = dropdown.getBoundingClientRect();
if (dropdownRect.right > window.innerWidth - 10) {
dropdown.style.left = `${window.innerWidth - dropdownRect.width - 10}px`;
}
if (dropdownRect.bottom > window.innerHeight - 10) {
dropdown.style.top = `${anchorRect.top - dropdownRect.height - 4}px`;
}
// Close on click outside
setTimeout(() => {
document.addEventListener('click', closeDropdownOnClickOutside);
}, 10);
}
function closeDropdownOnClickOutside(e) {
const dropdown = document.getElementById('node-dropdown');
if (dropdown && !dropdown.contains(e.target)) {
hideDropdown();
}
}
function hideDropdown() {
const dropdown = document.getElementById('node-dropdown');
if (dropdown) {
dropdown.remove();
}
document.removeEventListener('click', closeDropdownOnClickOutside);
currentDropdownAnchorRect = null;
pendingDropdownType = null;
}
function selectNode(nodeId) {
// Send node selection to main window
if (channel) {
channel.postMessage({ type: 'node-select', data: nodeId });
}
}
function markConnected() {
if (!isConnected) {
isConnected = true;