From 33425a9c6c67c1f6033c79802e45e01bd1928705 Mon Sep 17 00:00:00 2001 From: Alex Little Date: Thu, 4 Jun 2026 19:34:18 +0100 Subject: [PATCH] fix(ui): modal drag + removed startDrag func (#2430) * fixed * removed legacy startDrag fc, unified modal dragging * fixes post feedback --- static/app.js | 107 ++++++++++++---------------------------- static/js/windowDrag.js | 7 +++ static/sw.js | 2 +- 3 files changed, 40 insertions(+), 76 deletions(-) diff --git a/static/app.js b/static/app.js index 683e0e5..8593da3 100644 --- a/static/app.js +++ b/static/app.js @@ -13,6 +13,7 @@ import chatModule from './js/chat.js'; import compareModule from './js/compare/index.js'; import documentModule from './js/document.js'; import searchChatModule from './js/search-chat.js'; +import { makeWindowDraggable } from './js/windowDrag.js'; import markdownModule from './js/markdown.js'; import chatRenderer from './js/chatRenderer.js'; import sessionModule from './js/sessions.js'; @@ -2683,82 +2684,38 @@ function initializeEventListeners() { // Apply saved visibility on load applyUIVis(loadUIVis()); - // Generic draggable for all .modal elements - const _sharedDragModalIds = new Set(['settings-modal']); - try { document.querySelectorAll('.modal').forEach(m => { - if (_sharedDragModalIds.has(m.id)) return; - const content = m.querySelector('.modal-content'); - const header = m.querySelector('.modal-header'); - if (!content || !header) return; - let dragX, dragY, startLeft, startTop, dragging = false; - - // Reset to flex-centered position each time modal opens - new MutationObserver(() => { - if (!m.classList.contains('hidden')) { - content.style.position = ''; - content.style.left = ''; - content.style.top = ''; - content.style.right = ''; - content.style.bottom = ''; - content.style.margin = ''; - } - }).observe(m, { attributes: true, attributeFilter: ['class'] }); - - function startDrag(clientX, clientY) { - dragging = true; - const rect = content.getBoundingClientRect(); - dragX = clientX; dragY = clientY; - startLeft = rect.left; startTop = rect.top; - // Switch to fixed so it can be freely positioned - content.style.position = 'fixed'; - content.style.left = startLeft + 'px'; - content.style.top = startTop + 'px'; - content.style.margin = '0'; - } - - header.addEventListener('mousedown', (e) => { - if (e.target.closest('.close-btn')) return; - e.preventDefault(); - startDrag(e.clientX, e.clientY); - document.addEventListener('mousemove', onDrag); - document.addEventListener('mouseup', stopDrag); - }); - function onDrag(e) { - if (!dragging) return; - content.style.left = (startLeft + e.clientX - dragX) + 'px'; - content.style.top = (startTop + e.clientY - dragY) + 'px'; - } - function stopDrag() { - dragging = false; - document.removeEventListener('mousemove', onDrag); - document.removeEventListener('mouseup', stopDrag); - } - - // Touch drag is desktop-only — on mobile, modals are bottom sheets and - // ui.js handles swipe-down-to-dismiss. Attaching this listener fights - // the swipe-dismiss gesture. - if (window.innerWidth > 768) { - header.addEventListener('touchstart', (e) => { - if (e.target.closest('.close-btn')) return; - const t = e.touches[0]; - startDrag(t.clientX, t.clientY); - document.addEventListener('touchmove', onTouchDrag, { passive: false }); - document.addEventListener('touchend', stopTouchDrag); + // The only two modals without a per-module makeWindowDraggable call. Wire + // them onto the shared helper, drag-only, to match their old behavior. + try { + ['custom-preset-modal', 'rename-session-modal'].forEach((id) => { + const m = document.getElementById(id); + if (!m) return; + const content = m.querySelector('.modal-content'); + const header = m.querySelector('.modal-header'); + if (!content || !header) return; + makeWindowDraggable(m, { + content, header, + skipSelector: '.close-btn', + enableDock: false, + enableResize: false, }); - } - function onTouchDrag(e) { - if (!dragging) return; - e.preventDefault(); - const t = e.touches[0]; - content.style.left = (startLeft + t.clientX - dragX) + 'px'; - content.style.top = (startTop + t.clientY - dragY) + 'px'; - } - function stopTouchDrag() { - dragging = false; - document.removeEventListener('touchmove', onTouchDrag); - document.removeEventListener('touchend', stopTouchDrag); - } - }); } catch(e) { console.error('Modal drag init error:', e); } + // Re-center on open (these persist in the DOM). Guard on the + // hidden→visible edge so it never fires mid-drag. + let wasHidden = m.classList.contains('hidden'); + new MutationObserver(() => { + const isHidden = m.classList.contains('hidden'); + if (wasHidden && !isHidden) { + content.style.position = ''; + content.style.left = ''; + content.style.top = ''; + content.style.right = ''; + content.style.bottom = ''; + content.style.margin = ''; + } + wasHidden = isHidden; + }).observe(m, { attributes: true, attributeFilter: ['class'] }); + }); + } catch (e) { console.error('Dialog drag init error:', e); } })(); // ── Modal minimize → dock ── diff --git a/static/js/windowDrag.js b/static/js/windowDrag.js index e633bc6..7c16a53 100644 --- a/static/js/windowDrag.js +++ b/static/js/windowDrag.js @@ -149,6 +149,13 @@ export function makeWindowDraggable(modal, options = {}) { const _startDrag = (cx, cy) => { dragging = true; if (modal) modal.classList.add('modal-dragging'); + // Cancel any in-flight open animation so we don't pin a mid-animation + // rect and then jump once the animation settles. + try { + content.getAnimations() + .filter(a => a.playState !== 'finished') + .forEach(a => a.cancel()); + } catch (_) {} const rect = content.getBoundingClientRect(); if (onDragStart) { try { onDragStart({ rect, cx, cy }); } catch (_) {} diff --git a/static/sw.js b/static/sw.js index 755dcf4..f927c2b 100644 --- a/static/sw.js +++ b/static/sw.js @@ -7,7 +7,7 @@ // - Other static assets (images/fonts/libs): cache-first with bg refresh. // - API / non-GET: never cached. // Bump CACHE_NAME whenever the precache list or SW logic changes. -const CACHE_NAME = 'odysseus-v326'; +const CACHE_NAME = 'odysseus-v327'; // Core shell precached on install so repeat opens are instant without any // network wait. Keep this list in sync with the