feat: enable node list display and selection in the pop-out viewer and update version.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Vendored
+1
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user