import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; import { getSvgIcon } from "../../utils/icon_utils.js"; export const dialogShellMethods = { createHeader() { // Create tabs this.missingTab = $el("button.mr-tab.mr-tab-active", { onclick: () => this.switchTab('missing') }, [$el("span.mr-tab-label", { textContent: "Missing Models" })]); this.loadedTab = $el("button.mr-tab", { onclick: () => this.switchTab('loaded') }, [$el("span.mr-tab-label", { textContent: "Loaded Models" })]); this.optionsTab = $el("button.mr-tab", { onclick: () => this.switchTab('options') }, [$el("span.mr-tab-label", { textContent: "Options" })]); if (this.activeTab === 'missing') { this.updateTabButtonStates(); } const dragHandle = $el("div", { id: "model-resolver-drag-handle", ondragstart: (e) => e.preventDefault() }, [ $el("span", { textContent: "⠿" }) ]); this.setTooltip(dragHandle, "Drag window"); const fullscreenButton = $el("button", { id: "model-resolver-fullscreen-toggle", className: "mr-window-btn mr-window-btn--fullscreen", innerHTML: getSvgIcon('windowMaximize', 'currentColor', 'mr-window-btn-icon'), ariaLabel: "Toggle full screen", onclick: () => this.toggleFullScreen() }); this.setTooltip(fullscreenButton, "Toggle full screen"); this.dockButton = $el("button", { id: "model-resolver-dock-toggle", className: "mr-window-btn mr-window-btn--dock", innerHTML: getSvgIcon('internalLink', 'currentColor', 'mr-window-btn-icon'), ariaLabel: "Dock Model Resolver to sidebar", onclick: () => this.dockToSidebar() }); this.setTooltip(this.dockButton, "Dock to sidebar"); this.undockButton = $el("button", { id: "model-resolver-undock-toggle", className: "mr-window-btn mr-window-btn--undock", innerHTML: getSvgIcon('externalLink', 'currentColor', 'mr-window-btn-icon'), ariaLabel: "Undock Model Resolver", onclick: () => this.undockToFloating() }); this.setTooltip(this.undockButton, "Undock to floating window"); return $el("div.mr-dialog-shell", {}, [ $el("div.mr-dialog-topbar", {}, [ $el("div.mr-dialog-brand", {}, [ dragHandle, $el("div.mr-tabs", {}, [ this.missingTab, this.loadedTab, this.optionsTab ]) ]), $el("div.mr-dialog-controls", {}, [ this.dockButton, this.undockButton, fullscreenButton, $el("button", { className: "mr-window-btn mr-window-btn--close", innerHTML: getSvgIcon('x', 'currentColor', 'mr-window-btn-icon'), ariaLabel: "Close Model Resolver", onclick: () => this.close() }) ]) ]) ]); }, captureFloatingRect() { if (!this.element || this.docked || getComputedStyle(this.element).display === 'none') return; const rect = this.element.getBoundingClientRect(); if (!rect.width || !rect.height) return; this._floatingRectBeforeDock = { top: Math.round(rect.top), left: Math.round(rect.left), width: Math.round(rect.width), height: Math.round(rect.height) }; }, clearModalPlacementStyles() { if (!this.element) return; [ 'position', 'top', 'left', 'right', 'bottom', 'inset', 'transform', 'width', 'height', 'maxWidth', 'maxHeight', 'minWidth', 'minHeight', 'borderRadius', 'resize' ].forEach((property) => { this.element.style[property] = ''; }); }, restoreFloatingGeometry() { const el = this.element; if (!el) return; el.style.width = '1100px'; el.style.height = '700px'; el.style.maxWidth = '100vw'; el.style.maxHeight = '100vh'; el.style.minWidth = '640px'; el.style.minHeight = '420px'; el.style.resize = 'both'; el.style.borderRadius = '7px'; const rect = this._floatingRectBeforeDock; if (rect?.width && rect?.height) { el.style.width = `${rect.width}px`; el.style.height = `${rect.height}px`; el.style.top = `${rect.top}px`; el.style.left = `${rect.left}px`; el.style.transform = 'none'; return; } try { const wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); if (wh?.w && wh?.h) { el.style.width = `${wh.w}px`; el.style.height = `${wh.h}px`; } const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null'); if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) { el.style.top = `${pos.top}px`; el.style.left = `${pos.left}px`; el.style.transform = 'none'; return; } } catch (e) { // Fall back to centered floating geometry below. } el.style.top = '50%'; el.style.left = '50%'; el.style.transform = 'translate(-50%, -50%)'; }, rememberSidebarOpenMode(mode) { try { localStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked'); } catch (e) {} }, shouldOpenFromSidebarFloating() { try { return localStorage.getItem(this.sidebarOpenModeStorageKey) === 'floating'; } catch (e) { return false; } }, isVisible() { if (!this.element?.isConnected) return false; return getComputedStyle(this.element).display !== 'none'; }, dockTo(container) { if (!container || !this.element) return; if (!this.docked) { this.captureFloatingRect(); } if (this.fullscreen) { this.setFullScreen(false); } this.docked = true; this.dockContainer = container; this.lastDockContainer = container; this.pendingDockToSidebar = false; this.rememberSidebarOpenMode('docked'); this.backdrop.style.display = 'none'; container.classList.add('mr-sidebar-dock-panel'); this.element.classList.add('mr-is-docked'); this.clearModalPlacementStyles(); this.element.style.display = 'flex'; if (this.element.parentNode !== container) { container.appendChild(this.element); } }, dockToSidebar() { if (this.docked) return; this.rememberSidebarOpenMode('docked'); this.pendingDockToSidebar = true; if (this.isUsableDockContainer(this.lastDockContainer)) { this.dockTo(this.lastDockContainer); return; } this.tryOpenComfySidebarState(); requestAnimationFrame(() => { if (!this.pendingDockToSidebar || this.docked) return; const button = document.querySelector(`.${this.sidebarTabId}-tab-button`); if (button instanceof HTMLElement) { const isActive = button.matches([ '[aria-pressed="true"]', '[aria-selected="true"]', '[data-active="true"]', '[data-selected="true"]', '.active', '.is-active', '.selected', '.p-highlight' ].join(',')); if (!isActive) { button.click(); } } }); }, isUsableDockContainer(container) { if (!(container instanceof HTMLElement) || !container.isConnected) return false; const style = getComputedStyle(container); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = container.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }, trySetSidebarStateProperty(target, property, value) { try { if (!(property in target)) return false; return Reflect.set(target, property, value); } catch (error) { return false; } }, tryOpenComfySidebarState() { const extensionManager = app.extensionManager; let opened = false; const candidates = [ extensionManager?.sidebarTab, extensionManager?.sidebarTabs, extensionManager?.sidebar, extensionManager ].filter(Boolean); for (const candidate of candidates) { try { if (typeof candidate.openSidebar === 'function') { candidate.openSidebar(this.sidebarTabId); opened = true; } if (typeof candidate.openSidebarTab === 'function') { candidate.openSidebarTab(this.sidebarTabId); opened = true; } if (typeof candidate.setActiveSidebarTab === 'function') { candidate.setActiveSidebarTab(this.sidebarTabId); opened = true; } if (typeof candidate.setActiveSidebarTabId === 'function') { candidate.setActiveSidebarTabId(this.sidebarTabId); opened = true; } if (this.trySetSidebarStateProperty(candidate, 'activeSidebarTabId', this.sidebarTabId)) { opened = true; } } catch (error) { // Sidebar internals differ between ComfyUI versions; unsupported state APIs are ignored. } } return opened; }, undockToFloating({ persist = true, closeSidebar = true } = {}) { if (!this.element) return; const wasDocked = this.docked; const dockContainer = this.dockContainer; this.docked = false; this.dockContainer = null; this.element.classList.remove('mr-is-docked'); this.clearModalPlacementStyles(); if (this.element.parentNode !== document.body) { document.body.appendChild(this.element); } this.element.style.display = 'flex'; this.restoreFloatingGeometry(); this.ensureModalHandleInViewport({ persist }); if (wasDocked && closeSidebar) { this.closeComfySidebar(dockContainer); } }, closeComfySidebar(dockContainer = null) { const closedByState = this.tryCloseComfySidebarState(); requestAnimationFrame(() => { if (dockContainer && !dockContainer.isConnected) return; const button = document.querySelector(`.${this.sidebarTabId}-tab-button`); if (!(button instanceof HTMLElement)) return; const isActive = button.matches([ '[aria-pressed="true"]', '[aria-selected="true"]', '[data-active="true"]', '[data-selected="true"]', '.active', '.is-active', '.selected', '.p-highlight' ].join(',')); if (!closedByState || isActive) { button.click(); } }); }, tryCloseComfySidebarState() { const extensionManager = app.extensionManager; let closed = false; const candidates = [ extensionManager?.sidebarTab, extensionManager?.sidebarTabs, extensionManager?.sidebar, extensionManager ].filter(Boolean); for (const candidate of candidates) { try { if (typeof candidate.closeSidebar === 'function') { candidate.closeSidebar(); closed = true; } if (typeof candidate.closeSidebarTab === 'function') { candidate.closeSidebarTab(this.sidebarTabId); closed = true; } if (typeof candidate.setActiveSidebarTab === 'function') { candidate.setActiveSidebarTab(null); closed = true; } if (typeof candidate.setActiveSidebarTabId === 'function') { candidate.setActiveSidebarTabId(null); closed = true; } if ( this.trySetSidebarStateProperty(candidate, 'activeSidebarTabId', null) || this.trySetSidebarStateProperty(candidate, 'activeSidebarTab', null) ) { closed = true; } } catch (error) { // Sidebar internals differ between ComfyUI versions; unsupported state APIs are ignored. } } return closed; }, // Toggle full screen mode for the dialog toggleFullScreen() { this.setFullScreen(!this.fullscreen); }, setFullScreen(enable) { const shouldReturnToDocked = !enable && this.returnToDockedAfterFullscreen; if (enable && this.docked) { this.returnToDockedAfterFullscreen = true; this.undockToFloating({ persist: false }); } else if (enable) { this.returnToDockedAfterFullscreen = false; } this.fullscreen = !!enable; const el = this.element; if (!el) return; const btn = document.getElementById('model-resolver-fullscreen-toggle'); if (enable) { // Save current size try { const rect = el.getBoundingClientRect(); localStorage.setItem('model_resolver_modal_size_before_fs', JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) })); } catch (e) {} el.style.top = '0'; el.style.left = '0'; el.style.transform = 'none'; el.style.width = '100vw'; el.style.height = '100vh'; el.style.maxWidth = '100vw'; el.style.maxHeight = '100vh'; el.style.borderRadius = '0'; el.style.resize = 'none'; if (btn) { btn.innerHTML = getSvgIcon('windowRestore', 'currentColor', 'mr-window-btn-icon'); btn.setAttribute('aria-label', 'Exit full screen'); this.setTooltip(btn, 'Exit full screen'); } try { localStorage.setItem('model_resolver_modal_fullscreen', '1'); } catch (e) {} } else { this.returnToDockedAfterFullscreen = false; // Restore centered sizing el.style.maxWidth = '100vw'; el.style.maxHeight = '100vh'; el.style.borderRadius = '8px'; el.style.resize = 'both'; // Restore saved pre-FS size if available let wh = null; try { wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (e) {} if (wh && wh.w && wh.h) { el.style.width = `${wh.w}px`; el.style.height = `${wh.h}px`; } else { el.style.width = '1100px'; el.style.height = '700px'; } // Restore last known position if available, else center try { const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null'); if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) { el.style.top = `${pos.top}px`; el.style.left = `${pos.left}px`; el.style.transform = 'none'; } else { el.style.top = '50%'; el.style.left = '50%'; el.style.transform = 'translate(-50%, -50%)'; } } catch (e) { el.style.top = '50%'; el.style.left = '50%'; el.style.transform = 'translate(-50%, -50%)'; } if (btn) { btn.innerHTML = getSvgIcon('windowMaximize', 'currentColor', 'mr-window-btn-icon'); btn.setAttribute('aria-label', 'Enter full screen'); this.setTooltip(btn, 'Enter full screen'); } this.ensureModalHandleInViewport({ persist: true }); try { localStorage.setItem('model_resolver_modal_fullscreen', '0'); } catch (e) {} if (shouldReturnToDocked) { this.dockToSidebar(); } } }, getViewportClampedModalPosition(top, left) { const el = this.element; if (!el) return { top, left }; const vw = window.innerWidth || document.documentElement.clientWidth || 0; const vh = window.innerHeight || document.documentElement.clientHeight || 0; const pad = 4; const handle = document.getElementById('model-resolver-drag-handle'); if (handle) { const elRect = el.getBoundingClientRect(); const handleRect = handle.getBoundingClientRect(); const handleOffsetLeft = handleRect.left - elRect.left; const handleOffsetTop = handleRect.top - elRect.top; const handleWidth = handleRect.width || handle.offsetWidth; const handleHeight = handleRect.height || handle.offsetHeight; const minLeft = pad - handleOffsetLeft; const maxLeft = vw - pad - handleOffsetLeft - handleWidth; const minTop = pad - handleOffsetTop; const maxTop = vh - pad - handleOffsetTop - handleHeight; left = Math.max(minLeft, Math.min(maxLeft, left)); top = Math.max(minTop, Math.min(maxTop, top)); } else { const w = el.offsetWidth; const h = el.offsetHeight; left = Math.max(-w + pad, Math.min(vw - pad, left)); top = Math.max(-h + pad, Math.min(vh - pad, top)); } return { top, left }; }, saveModalPosition() { if (this.docked) return; try { const el = this.element; if (!el) return; const rect = el.getBoundingClientRect(); localStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) })); } catch (e) { /* ignore */ } }, ensureModalHandleInViewport({ persist = false } = {}) { if (this.docked) return; if (this.fullscreen) return; const el = this.element; if (!el || getComputedStyle(el).display === 'none') return; const rect = el.getBoundingClientRect(); const { top, left } = this.getViewportClampedModalPosition(rect.top, rect.left); const nextTop = Math.round(top); const nextLeft = Math.round(left); if (Math.round(rect.top) === nextTop && Math.round(rect.left) === nextLeft) return; el.style.top = `${nextTop}px`; el.style.left = `${nextLeft}px`; el.style.transform = 'none'; if (persist) this.saveModalPosition(); }, scheduleModalViewportClamp(persist = false) { if (this._viewportClampFrame) { cancelAnimationFrame(this._viewportClampFrame); } this._viewportClampFrame = requestAnimationFrame(() => { this._viewportClampFrame = null; this.ensureModalHandleInViewport({ persist }); }); }, // Begin window drag startDrag(e) { if (this.docked) return; try { const el = this.element; if (!el) return; const rect = el.getBoundingClientRect(); // Switch to absolute top/left (no transform) before dragging el.style.top = `${rect.top}px`; el.style.left = `${rect.left}px`; el.style.transform = 'none'; this._dragging = true; this._dragStart = { x: e.clientX, y: e.clientY, top: rect.top, left: rect.left }; // Prevent text selection while dragging this._prevUserSelect = document.body.style.userSelect; document.body.style.userSelect = 'none'; // Attach listeners this._onMouseMove = (ev) => this.onDrag(ev); this._onMouseUp = () => this.endDrag(); document.addEventListener('mousemove', this._onMouseMove); document.addEventListener('mouseup', this._onMouseUp, { once: true }); } catch (err) { /* ignore */ } }, onDrag(e) { if (!this._dragging || !this._dragStart) return; const el = this.element; if (!el) return; const dx = e.clientX - this._dragStart.x; const dy = e.clientY - this._dragStart.y; let top = this._dragStart.top + dy; let left = this._dragStart.left + dx; ({ top, left } = this.getViewportClampedModalPosition(top, left)); el.style.top = `${Math.round(top)}px`; el.style.left = `${Math.round(left)}px`; }, endDrag() { if (!this._dragging) return; this._dragging = false; document.removeEventListener('mousemove', this._onMouseMove); // Persist position this.saveModalPosition(); // Restore selection try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) {} }, /** * Simple debounce helper */ debounce(callback, wait = 250) { let t = null; return (...args) => { if (t) clearTimeout(t); t = setTimeout(() => { callback.apply(this, args); }, wait); }; } };