Files
ComfyUI-OpenClaw/web/openclaw_tabs.js
T

174 lines
6.3 KiB
JavaScript

/**
* F7: Tab Manager
* Handles tab creation, switching, and lazy rendering.
*/
import { ErrorBoundary } from "./ErrorBoundary.js";
import { STORAGE_KEYS, getMirroredStorageValue, setMirroredStorageValue } from "./openclaw_compat.js";
import {
applyLegacyClassAliases,
appendChildren,
createDomElement,
normalizeLegacyClassNames,
} from "./openclaw_utils.js";
/**
* @typedef {Object} OpenClawTabDefinition
* @property {string} id Stable tab id used for storage and pane ids.
* @property {string} title Visible tab label and error-boundary name.
* @property {string=} icon Optional icon class for the tab button.
* @property {(pane: HTMLElement) => (void|Promise<void>)} render Render function for the tab pane.
* @property {(pane: HTMLElement) => boolean=} dispose Optional pending-render cleanup; true requests rerender.
* @property {boolean=} loaded Internal lazy-render state.
*/
export class TabManager {
constructor() {
/** @type {OpenClawTabDefinition[]} */
this.tabs = [
// { id, title, renderFn, loaded }
];
this.tabsEl = null;
this.contentEl = null;
this.activeTabId = null;
}
init(tabsEl, contentEl) {
this.tabsEl = tabsEl;
this.contentEl = contentEl;
// IMPORTANT: ComfyUI may remount extension DOM.
// When containers are new, previously-rendered tabs must be re-rendered.
this.tabs = this.tabs.map(t => ({ ...t, loaded: false }));
this._loadTabs();
this._restoreActiveTab();
}
/**
* Register or replace a tab definition.
* @param {OpenClawTabDefinition} tabDef
*/
registerTab(tabDef) {
// Idempotent: replace existing tab definition by id
const idx = this.tabs.findIndex(t => t.id === tabDef.id);
if (idx >= 0) {
this.tabs[idx] = { ...tabDef, loaded: false };
} else {
this.tabs.push({ ...tabDef, loaded: false });
}
// If initialized, re-render
if (this.tabsEl) this._renderTabs();
}
_loadTabs() {
// In a real implementation, we might auto-discover tabs.
// For now, tabs are registered via exposed API or by importing them in entry point.
this._renderTabs();
}
_renderTabs() {
this.tabsEl.innerHTML = "";
this.tabs.forEach(tab => {
const btn = createDomElement("div", { className: "openclaw-tab" });
if (tab.icon) {
appendChildren(btn, [
createDomElement("i", {
className: `openclaw-tab-icon ${tab.icon}`,
}),
createDomElement("span", {
className: "openclaw-tab-label",
text: tab.title,
}),
]);
} else {
btn.textContent = tab.title;
}
btn.onclick = () => this.activateTab(tab.id);
if (tab.id === this.activeTabId) btn.classList.add("active");
this.tabsEl.appendChild(btn);
// Create container for tab content if not exists
if (!this.contentEl.querySelector(`#openclaw-tab-${tab.id}`)) {
const pane = createDomElement("div", {
className: "openclaw-tab-pane",
});
pane.id = `openclaw-tab-${tab.id}`;
this.contentEl.appendChild(pane);
}
});
normalizeLegacyClassNames(this.tabsEl);
normalizeLegacyClassNames(this.contentEl);
applyLegacyClassAliases(this.tabsEl);
applyLegacyClassAliases(this.contentEl);
}
activateTab(id) {
const previousId = this.activeTabId;
if (previousId && previousId !== id) {
const previousTab = this.tabs.find(tab => tab.id === previousId);
const previousPane =
this.contentEl.querySelector(`#openclaw-tab-${previousId}`) ||
this.contentEl.querySelector(`#moltbot-tab-${previousId}`);
if (previousTab?.dispose && previousPane) {
// IMPORTANT: invalidate pending async tab work before hiding its owner pane.
const requiresRerender = previousTab.dispose(previousPane) === true;
if (requiresRerender) {
previousTab.loaded = false;
previousPane.replaceChildren();
}
}
}
this.activeTabId = id;
setMirroredStorageValue(localStorage, STORAGE_KEYS.local.activeTab, id);
// Update Tab Buttons
Array.from(this.tabsEl.children).forEach((btn, idx) => {
const tab = this.tabs[idx];
if (tab.id === id) btn.classList.add("active");
else btn.classList.remove("active");
});
// Update Panes
Array.from(this.contentEl.children).forEach(pane => {
if (pane.id === `openclaw-tab-${id}` || pane.id === `moltbot-tab-${id}`) {
pane.classList.add("active");
}
else pane.classList.remove("active");
});
// Lazy Render
const tab = this.tabs.find(t => t.id === id);
const pane =
this.contentEl.querySelector(`#openclaw-tab-${id}`) ||
this.contentEl.querySelector(`#moltbot-tab-${id}`);
const shouldRender = tab && pane && (!tab.loaded || !pane.hasChildNodes());
if (shouldRender) {
const boundary = new ErrorBoundary(`Tab: ${tab.title}`);
boundary.run(pane, () => {
const maybePromise = tab.render(pane);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch((err) => boundary.showFallback(pane, err));
}
});
tab.loaded = true;
}
if (pane) {
normalizeLegacyClassNames(pane);
applyLegacyClassAliases(pane);
}
}
_restoreActiveTab() {
// CRITICAL: keep legacy key fallback to avoid tab-state loss across migration.
const saved = getMirroredStorageValue(localStorage, STORAGE_KEYS.local.activeTab);
const defaultTab = this.tabs.length > 0 ? this.tabs[0].id : null;
this.activateTab(saved && this.tabs.find(t => t.id === saved) ? saved : defaultTab);
}
}
export const tabManager = new TabManager(); // Singleton for easy registration