Files
MajoorWaldi-ComfyUI-Majoor-…/ui/features/runtime/topBarMfvButton.ts
T

378 lines
11 KiB
TypeScript

import { EVENTS } from "../../app/events.js";
import { t } from "../../app/i18n.js";
import { setTooltipHint } from "../../utils/tooltipShortcuts.js";
const BUTTON_HOST_ATTR = "data-mjr-topbar-mfv-host";
const BUTTON_ATTR = "data-mjr-topbar-mfv-button";
const SLOT_ATTR = "data-mjr-topbar-mfv-slot";
const BUTTON_LABEL_KEY = "label.floatingViewer";
const BUTTON_LABEL_FALLBACK = "Viewer";
const MFV_TOOLTIP_HINT = "V";
const ACTIONBAR_SELECTORS = [
".actionbar-container",
"[data-testid='actionbar-container']",
"[data-testid='topbar']",
".comfyui-topbar",
".topbar",
];
const BUTTON_GROUP_SELECTORS = [
".queue-button-group",
".comfyui-button-group",
"[role='toolbar']",
];
const MANAGER_BUTTON_SELECTORS = [
"[data-testid*='manager' i]",
"[aria-label*='manager' i]",
"[title*='manager' i]",
"[data-command-id*='manager' i]",
"[data-command-id*='mjr.openAssetsManager' i]",
"button",
];
let _observer: any = null;
let _observedTarget: any = null;
let _bodyObserver: any = null;
let _visibilityListener: any = null;
let _resizeListener: any = null;
let _syncQueued = false;
let _syncTimer: any = null;
let _visible = false;
let _hasVisibilitySignal = false;
function _tryObserveActionbar(container: any) {
if (_observer && _observedTarget === container) return;
if (!container) {
// Actionbar not in DOM yet — observe body shallowly to detect it
if (!_bodyObserver && typeof MutationObserver !== "undefined") {
_bodyObserver = new MutationObserver(() => {
const c = getActionbarContainer();
if (c) {
_bodyObserver?.disconnect?.();
_bodyObserver = null;
scheduleSync();
}
});
_bodyObserver.observe(document.body, { childList: true, subtree: true });
}
return;
}
// Disconnect previous observer if target changed
try {
_observer?.disconnect?.();
} catch (_) {
/* noop */
}
_observer = new MutationObserver(() => scheduleSync());
_observer.observe(container, { childList: true, subtree: true });
_observedTarget = container;
// Body observer no longer needed
try {
_bodyObserver?.disconnect?.();
} catch (_) {
/* noop */
}
_bodyObserver = null;
}
function getActionbarContainer() {
if (typeof document === "undefined") return null;
for (const selector of ACTIONBAR_SELECTORS) {
const el = document.querySelector(selector);
if (el) return el;
}
return null;
}
function updateViewerTopOffset(container = getActionbarContainer()) {
if (typeof document === "undefined") return;
const rootStyle = document.documentElement?.style;
if (!rootStyle) return;
if (!container) {
rootStyle.setProperty("--mjr-mfv-top-offset", "60px");
return;
}
const rect = container.getBoundingClientRect();
const safeTop = Math.max(60, Math.ceil(rect.bottom + 12));
rootStyle.setProperty("--mjr-mfv-top-offset", `${safeTop}px`);
}
function getAnchor(container: any) {
if (!container) return null;
const managerButton = getManagerButtonAnchor(container);
if (managerButton) return managerButton;
for (const selector of BUTTON_GROUP_SELECTORS) {
const el = container.querySelector(selector);
if (el) return el;
}
return null;
}
function isManagerLikeButton(el: any) {
try {
const text = String(el?.textContent || "").trim().toLowerCase();
const label = String(
el?.getAttribute?.("aria-label") ||
el?.getAttribute?.("title") ||
el?.getAttribute?.("data-testid") ||
el?.getAttribute?.("data-command-id") ||
"",
).toLowerCase();
return (
text === "manager" ||
text.includes("assets manager") ||
text.includes("majoor assets manager") ||
label.includes("manager") ||
label.includes("mjr.openassetsmanager")
);
} catch {
return false;
}
}
function getManagerButtonAnchor(container: any) {
if (!container?.querySelectorAll) return null;
for (const selector of MANAGER_BUTTON_SELECTORS) {
try {
const candidates = Array.from(container.querySelectorAll(selector));
const found = candidates.find((el: any) => isManagerLikeButton(el));
if (found) return found;
} catch (e) {
console.debug?.(e);
}
}
return null;
}
function getTopbarButtonParent(container: any, anchor: any) {
if (!container) return null;
if (anchor?.classList?.contains?.("comfyui-button-group")) return anchor;
return anchor?.parentElement || container;
}
function ensureSlotMounted(container: any) {
if (!container) return null;
let slot = container.querySelector(`[${SLOT_ATTR}]`);
if (!slot) {
slot = document.createElement("div");
slot.setAttribute(SLOT_ATTR, "1");
slot.className = "flex h-full items-center pointer-events-auto";
slot.style.position = "relative";
slot.style.zIndex = "10030";
slot.style.padding = "0 4px";
}
const anchor = getAnchor(container);
const parent = getTopbarButtonParent(container, anchor);
if (slot.parentElement !== parent) {
if (anchor && parent && parent !== anchor) {
parent.insertBefore(slot, anchor.nextSibling);
} else if (parent) {
parent.appendChild(slot);
}
} else if (anchor && parent && parent !== anchor && slot.previousSibling !== anchor) {
parent.insertBefore(slot, anchor.nextSibling);
} else if (!anchor && slot !== parent.lastElementChild) {
parent.appendChild(slot);
}
return slot;
}
function createIcon() {
const icon = document.createElement("i");
icon.className = "mjr-topbar-mfv-icon pi pi-eye";
icon.setAttribute("aria-hidden", "true");
return icon;
}
function createLabel(text = "Viewer") {
const label = document.createElement("span");
label.className = "mjr-topbar-mfv-label";
label.textContent = text;
return label;
}
function updateButtonState(button: any) {
if (!button) return;
const tooltipLabel = _visible
? t("tooltip.closeMFV", "Close Majoor Floating Viewer")
: t("tooltip.openMFV", "Open Majoor Floating Viewer");
setTooltipHint(button, tooltipLabel, MFV_TOOLTIP_HINT, {
ariaLabel: tooltipLabel,
});
button.setAttribute("aria-pressed", _visible ? "true" : "false");
button.classList.toggle("primary", _visible);
button.classList.toggle("mjr-topbar-mfv-active", _visible);
button.replaceChildren(
createIcon(),
createLabel(t(BUTTON_LABEL_KEY, BUTTON_LABEL_FALLBACK)),
);
}
function dispatchLaunch() {
try {
window.dispatchEvent(new Event(EVENTS.MFV_TOGGLE));
} catch (e) {
console.debug?.("[Majoor] top bar MFV launch failed", e);
}
scheduleSync();
}
function createButton() {
const button = document.createElement("button");
button.type = "button";
button.setAttribute(BUTTON_ATTR, "1");
button.setAttribute("data-command-id", "mjr.toggleFloatingViewer");
button.className = "comfyui-button p-button p-component mjr-topbar-mfv-button";
button.style.position = "relative";
button.style.zIndex = "10030";
button.style.width = "auto";
button.style.height = "32px";
button.style.minWidth = "32px";
button.style.padding = "0 8px";
button.style.gap = "8px";
button.style.display = "inline-flex";
button.style.alignItems = "center";
button.style.justifyContent = "center";
button.style.whiteSpace = "nowrap";
button.addEventListener("click", dispatchLaunch);
updateButtonState(button);
return button;
}
function createButtonHost() {
const host = document.createElement("div");
host.setAttribute(BUTTON_HOST_ATTR, "1");
host.className = "mjr-topbar-mfv-button-host";
host.style.position = "relative";
host.appendChild(createButton());
return host;
}
function _syncVisibleFromDom() {
if (typeof document === "undefined") return;
if (_hasVisibilitySignal) return;
const viewerOpen = !!document.querySelector(".mjr-mfv.is-visible");
if (_visible !== viewerOpen) {
_visible = viewerOpen;
}
}
function ensureButtonMounted() {
const container = getActionbarContainer();
if (!container) {
updateViewerTopOffset(null);
return null;
}
_tryObserveActionbar(container);
_syncVisibleFromDom();
const slot = ensureSlotMounted(container);
if (!slot) {
updateViewerTopOffset(container);
return null;
}
let host = slot.querySelector(`[${BUTTON_HOST_ATTR}]`);
if (!host) {
host = createButtonHost();
}
if (host.parentElement !== slot) {
slot.replaceChildren(host);
}
updateViewerTopOffset(container);
updateButtonState(host.querySelector(`[${BUTTON_ATTR}]`));
return slot;
}
function scheduleSync() {
if (_syncQueued) return;
_syncQueued = true;
_syncTimer = window.setTimeout(() => {
_syncQueued = false;
_syncTimer = null;
ensureButtonMounted();
}, 32);
}
export function mountTopBarMfvButton(): boolean {
if (typeof window === "undefined" || typeof document === "undefined" || !document.body) {
return false;
}
scheduleSync();
if (!_visibilityListener) {
_visibilityListener = (event: any) => {
_visible = Boolean(event?.detail?.visible);
_hasVisibilitySignal = true;
scheduleSync();
};
window.addEventListener(EVENTS.MFV_VISIBILITY_CHANGED, _visibilityListener);
}
if (!_resizeListener) {
_resizeListener = () => scheduleSync();
window.addEventListener("resize", _resizeListener);
}
_tryObserveActionbar(getActionbarContainer());
return true;
}
export function teardownTopBarMfvButton(): void {
try {
_observer?.disconnect?.();
} catch (e) {
console.debug?.(e);
}
_observer = null;
_observedTarget = null;
try {
_bodyObserver?.disconnect?.();
} catch (e) {
console.debug?.(e);
}
_bodyObserver = null;
try {
if (_syncTimer) {
window.clearTimeout(_syncTimer);
}
} catch (e) {
console.debug?.(e);
}
_syncTimer = null;
if (_visibilityListener && typeof window !== "undefined") {
window.removeEventListener(EVENTS.MFV_VISIBILITY_CHANGED, _visibilityListener);
}
_visibilityListener = null;
if (_resizeListener && typeof window !== "undefined") {
window.removeEventListener("resize", _resizeListener);
}
_resizeListener = null;
_syncQueued = false;
_visible = false;
_hasVisibilitySignal = false;
try {
document.documentElement?.style?.setProperty("--mjr-mfv-top-offset", "60px");
document.querySelector(`[${BUTTON_HOST_ATTR}]`)?.remove?.();
document.querySelector(`[${SLOT_ATTR}]`)?.remove?.();
} catch (e) {
console.debug?.(e);
}
}