From 34d4139ec6575e2858951ced27e09e2b6b65485b Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 27 May 2026 16:46:56 +0200 Subject: [PATCH] Implement viewport clamping for modal dialog and add resize handling --- web/linker.js | 106 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/web/linker.js b/web/linker.js index 3b7b52f..a46968d 100644 --- a/web/linker.js +++ b/web/linker.js @@ -37,6 +37,8 @@ class LinkerManagerDialog extends ComfyDialog { this._dragStart = null; this._analysisProgressToken = null; this._locateAnimationFrame = null; + this._viewportClampFrame = null; + this._boundHandleViewportResize = () => this.scheduleModalViewportClamp(true); // Create backdrop overlay for click-outside-to-close this.backdrop = $el("div.model-linker-backdrop", { @@ -102,6 +104,7 @@ class LinkerManagerDialog extends ComfyDialog { // Add click listener to hide context menu when clicking outside this.boundHandleContextMenuClick = (e) => this.handleContextMenuOutsideClick(e); document.addEventListener('click', this.boundHandleContextMenuClick); + window.addEventListener('resize', this._boundHandleViewportResize); } /** @@ -2125,10 +2128,83 @@ class LinkerManagerDialog extends ComfyDialog { el.style.transform = 'translate(-50%, -50%)'; } if (btn) btn.textContent = '⛶'; + this.ensureModalHandleInViewport({ persist: true }); try { localStorage.setItem('model_linker_modal_fullscreen', '0'); } catch (e) {} } } + 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-linker-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() { + try { + const el = this.element; + if (!el) return; + const rect = el.getBoundingClientRect(); + localStorage.setItem('model_linker_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) })); + } catch (e) { /* ignore */ } + } + + ensureModalHandleInViewport({ persist = false } = {}) { + 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) { try { @@ -2165,28 +2241,7 @@ class LinkerManagerDialog extends ComfyDialog { const dy = e.clientY - this._dragStart.y; let top = this._dragStart.top + dy; let left = this._dragStart.left + dx; - // Clamp so the drag handle always stays reachable on screen. - const vw = window.innerWidth; - const vh = window.innerHeight; - const pad = 4; // small padding - const handle = document.getElementById('model-linker-drag-handle'); - if (handle) { - const handleOffsetLeft = handle.offsetLeft; - const handleOffsetTop = handle.offsetTop; - const handleWidth = handle.offsetWidth; - const handleHeight = 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)); - } + ({ top, left } = this.getViewportClampedModalPosition(top, left)); el.style.top = `${Math.round(top)}px`; el.style.left = `${Math.round(left)}px`; } @@ -2196,11 +2251,7 @@ class LinkerManagerDialog extends ComfyDialog { this._dragging = false; document.removeEventListener('mousemove', this._onMouseMove); // Persist position - try { - const el = this.element; - const rect = el.getBoundingClientRect(); - localStorage.setItem('model_linker_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) })); - } catch (e) { /* ignore */ } + this.saveModalPosition(); // Restore selection try { document.body.style.userSelect = this._prevUserSelect || ''; } catch (e) {} } @@ -3689,6 +3740,7 @@ class LinkerManagerDialog extends ComfyDialog { async show(workflow = null) { this.backdrop.style.display = "none"; this.element.style.display = "flex"; + this.scheduleModalViewportClamp(); // Update button state in case there are active downloads this.updateDownloadAllButtonState();