Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/viewer/workflowSidebar/WorkflowSidebar.js
T

225 lines
7.2 KiB
JavaScript

/**
* WorkflowSidebar — slide-in panel for the Floating Viewer (MFV).
*
* Shows editable widgets for the currently selected ComfyUI nodes.
* Reads from `app.canvas.selected_nodes` via comfyApiBridge.
* Writes back via widgetAdapters (widget.value + callback).
*/
import { getComfyApp } from "../../../app/comfyApiBridge.js";
import { NodeWidgetRenderer } from "./NodeWidgetRenderer.js";
const LIVE_SYNC_FALLBACK_MS = 16;
export class WorkflowSidebar {
/**
* @param {object} opts
* @param {HTMLElement} opts.hostEl The MFV root element to append to
* @param {() => void} [opts.onClose]
*/
constructor({ hostEl, onClose } = {}) {
this._hostEl = hostEl;
this._onClose = onClose ?? null;
/** @type {NodeWidgetRenderer[]} */
this._renderers = [];
this._visible = false;
this._selectionKey = "";
this._liveSyncHandle = null;
this._liveSyncMode = "";
this._el = this._build();
}
get el() { return this._el; }
get isVisible() { return this._visible; }
show() {
this._visible = true;
this._el.classList.add("open");
this.refresh({ force: true });
this._startLiveSync();
}
hide() {
this._visible = false;
this._el.classList.remove("open");
this._stopLiveSync();
}
toggle() {
if (this._visible) {
this.hide();
this._onClose?.();
} else {
this.show();
}
}
/** Re-read selected nodes and rebuild widget sections. */
refresh({ force = false } = {}) {
if (!this._visible) return;
const { key, nodes } = _getSelectedNodeSnapshot();
if (!force && key === this._selectionKey && this._renderers.length === nodes.length) {
this.syncFromGraph({ allowSelectionRefresh: false });
return;
}
this._clear();
this._selectionKey = key;
if (!nodes.length) {
this._showEmpty();
return;
}
for (const node of nodes) {
const renderer = new NodeWidgetRenderer(node);
this._renderers.push(renderer);
this._body.appendChild(renderer.el);
}
this.syncFromGraph({ allowSelectionRefresh: false });
}
/** Sync existing renderers from graph values without full rebuild. */
syncFromGraph({ allowSelectionRefresh = true } = {}) {
if (!this._visible) return;
const { key } = _getSelectedNodeSnapshot();
if (allowSelectionRefresh && key !== this._selectionKey) {
this.refresh({ force: true });
return;
}
for (const r of this._renderers) r.syncFromGraph();
}
dispose() {
this._stopLiveSync();
this._clear();
this._el?.remove();
}
// ── Private ───────────────────────────────────────────────────────────────
_build() {
const panel = document.createElement("div");
panel.className = "mjr-ws-sidebar";
// Header
const header = document.createElement("div");
header.className = "mjr-ws-sidebar-header";
const title = document.createElement("span");
title.className = "mjr-ws-sidebar-title";
title.textContent = "Node Parameters";
header.appendChild(title);
const closeBtn = document.createElement("button");
closeBtn.type = "button";
closeBtn.className = "mjr-icon-btn";
closeBtn.title = "Close sidebar";
const closeIcon = document.createElement("i");
closeIcon.className = "pi pi-times";
closeIcon.setAttribute("aria-hidden", "true");
closeBtn.appendChild(closeIcon);
closeBtn.addEventListener("click", () => {
this.hide();
this._onClose?.();
});
header.appendChild(closeBtn);
panel.appendChild(header);
// Scrollable body
this._body = document.createElement("div");
this._body.className = "mjr-ws-sidebar-body";
panel.appendChild(this._body);
return panel;
}
_clear() {
for (const r of this._renderers) r.dispose();
this._renderers = [];
if (this._body) this._body.innerHTML = "";
}
_showEmpty() {
const empty = document.createElement("div");
empty.className = "mjr-ws-sidebar-empty";
empty.textContent = "Select nodes on the canvas to edit their parameters";
this._body.appendChild(empty);
}
_startLiveSync() {
if (this._liveSyncHandle != null) return;
const frameHost = _getFrameHost(this._el);
const tick = () => {
this._liveSyncHandle = null;
this._liveSyncMode = "";
if (!this._visible) return;
this.syncFromGraph();
this._startLiveSync();
};
if (typeof frameHost.requestAnimationFrame === "function") {
this._liveSyncMode = "raf";
this._liveSyncHandle = frameHost.requestAnimationFrame(tick);
return;
}
this._liveSyncMode = "timeout";
this._liveSyncHandle = frameHost.setTimeout(tick, LIVE_SYNC_FALLBACK_MS);
}
_stopLiveSync() {
if (this._liveSyncHandle == null) return;
const frameHost = _getFrameHost(this._el);
try {
if (this._liveSyncMode === "raf" && typeof frameHost.cancelAnimationFrame === "function") {
frameHost.cancelAnimationFrame(this._liveSyncHandle);
} else if (typeof frameHost.clearTimeout === "function") {
frameHost.clearTimeout(this._liveSyncHandle);
}
} catch (e) {
console.debug?.(e);
}
this._liveSyncHandle = null;
this._liveSyncMode = "";
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
/**
* Read the currently selected nodes from the ComfyUI canvas.
* Supports multiple ComfyUI versions (object, Map, Array).
*/
function _getSelectedNodes() {
try {
const app = getComfyApp();
const selected =
app?.canvas?.selected_nodes ??
app?.canvas?.selectedNodes ??
null;
if (!selected) return [];
if (Array.isArray(selected)) return selected.filter(Boolean);
if (selected instanceof Map) return Array.from(selected.values()).filter(Boolean);
if (typeof selected === "object") return Object.values(selected).filter(Boolean);
} catch (e) {
console.debug?.("[MFV sidebar] _getSelectedNodes error", e);
}
return [];
}
function _getSelectedNodeSnapshot() {
const nodes = _getSelectedNodes();
const key = nodes
.map((node) => String(node?.id ?? ""))
.filter(Boolean)
.join("|");
return { key, nodes };
}
function _getFrameHost(el) {
const view = el?.ownerDocument?.defaultView || null;
if (view) return view;
if (typeof window !== "undefined") return window;
return globalThis;
}