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

297 lines
8.6 KiB
JavaScript

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, Ctrl/Cmd+V";
let _observer = null;
let _observedTarget = null;
let _bodyObserver = null;
let _visibilityListener = null;
let _resizeListener = null;
let _syncQueued = false;
let _syncTimer = null;
let _visible = false;
let _hasVisibilitySignal = false;
function _tryObserveActionbar(container) {
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 });
_observedTarget = container;
// Body observer no longer needed
try { _bodyObserver?.disconnect?.(); } catch (_) { /* noop */ }
_bodyObserver = null;
}
function getActionbarContainer() {
if (typeof document === "undefined") return null;
return document.querySelector(".actionbar-container");
}
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) {
if (!container) return null;
return container.querySelector(".queue-button-group") || null;
}
function ensureSlotMounted(container) {
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 = anchor?.parentElement || container;
if (slot.parentElement !== parent) {
if (anchor && parent) {
parent.insertBefore(slot, anchor.nextSibling);
} else {
parent.appendChild(slot);
}
} else if (anchor && parent && slot.previousSibling !== anchor) {
parent.insertBefore(slot, anchor.nextSibling);
} else if (!anchor && slot !== parent.lastElementChild) {
parent.appendChild(slot);
}
return slot;
}
function createIcon(className = "pi pi-eye") {
const icon = document.createElement("i");
icon.className = className;
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) {
if (!button) return;
const tooltipLabel = _visible
? t("tooltip.closeMFV", "Close Floating Viewer")
: t("tooltip.openMFV", "Open 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(_visible ? "pi pi-eye-slash" : "pi pi-eye"),
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.className = "comfyui-button 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 10px";
button.style.gap = "6px";
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() {
if (typeof window === "undefined" || typeof document === "undefined" || !document.body) {
return false;
}
scheduleSync();
if (!_visibilityListener) {
_visibilityListener = (event) => {
_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() {
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);
}
}