fix(ui): modal drag + removed startDrag func (#2430)

* fixed

* removed legacy startDrag fc, unified modal dragging

* fixes post feedback
This commit is contained in:
Alex Little
2026-06-04 19:34:18 +01:00
committed by GitHub
parent ab5311c44d
commit 33425a9c6c
3 changed files with 40 additions and 76 deletions

View File

@@ -13,6 +13,7 @@ import chatModule from './js/chat.js';
import compareModule from './js/compare/index.js'; import compareModule from './js/compare/index.js';
import documentModule from './js/document.js'; import documentModule from './js/document.js';
import searchChatModule from './js/search-chat.js'; import searchChatModule from './js/search-chat.js';
import { makeWindowDraggable } from './js/windowDrag.js';
import markdownModule from './js/markdown.js'; import markdownModule from './js/markdown.js';
import chatRenderer from './js/chatRenderer.js'; import chatRenderer from './js/chatRenderer.js';
import sessionModule from './js/sessions.js'; import sessionModule from './js/sessions.js';
@@ -2683,82 +2684,38 @@ function initializeEventListeners() {
// Apply saved visibility on load // Apply saved visibility on load
applyUIVis(loadUIVis()); applyUIVis(loadUIVis());
// Generic draggable for all .modal elements // The only two modals without a per-module makeWindowDraggable call. Wire
const _sharedDragModalIds = new Set(['settings-modal']); // them onto the shared helper, drag-only, to match their old behavior.
try { document.querySelectorAll('.modal').forEach(m => { try {
if (_sharedDragModalIds.has(m.id)) return; ['custom-preset-modal', 'rename-session-modal'].forEach((id) => {
const content = m.querySelector('.modal-content'); const m = document.getElementById(id);
const header = m.querySelector('.modal-header'); if (!m) return;
if (!content || !header) return; const content = m.querySelector('.modal-content');
let dragX, dragY, startLeft, startTop, dragging = false; const header = m.querySelector('.modal-header');
if (!content || !header) return;
// Reset to flex-centered position each time modal opens makeWindowDraggable(m, {
new MutationObserver(() => { content, header,
if (!m.classList.contains('hidden')) { skipSelector: '.close-btn',
content.style.position = ''; enableDock: false,
content.style.left = ''; enableResize: false,
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);
}); });
} // Re-center on open (these persist in the DOM). Guard on the
function onTouchDrag(e) { // hidden→visible edge so it never fires mid-drag.
if (!dragging) return; let wasHidden = m.classList.contains('hidden');
e.preventDefault(); new MutationObserver(() => {
const t = e.touches[0]; const isHidden = m.classList.contains('hidden');
content.style.left = (startLeft + t.clientX - dragX) + 'px'; if (wasHidden && !isHidden) {
content.style.top = (startTop + t.clientY - dragY) + 'px'; content.style.position = '';
} content.style.left = '';
function stopTouchDrag() { content.style.top = '';
dragging = false; content.style.right = '';
document.removeEventListener('touchmove', onTouchDrag); content.style.bottom = '';
document.removeEventListener('touchend', stopTouchDrag); content.style.margin = '';
} }
}); } catch(e) { console.error('Modal drag init error:', e); } wasHidden = isHidden;
}).observe(m, { attributes: true, attributeFilter: ['class'] });
});
} catch (e) { console.error('Dialog drag init error:', e); }
})(); })();
// ── Modal minimize → dock ── // ── Modal minimize → dock ──

View File

@@ -149,6 +149,13 @@ export function makeWindowDraggable(modal, options = {}) {
const _startDrag = (cx, cy) => { const _startDrag = (cx, cy) => {
dragging = true; dragging = true;
if (modal) modal.classList.add('modal-dragging'); 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(); const rect = content.getBoundingClientRect();
if (onDragStart) { if (onDragStart) {
try { onDragStart({ rect, cx, cy }); } catch (_) {} try { onDragStart({ rect, cx, cy }); } catch (_) {}

View File

@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh. // - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached. // - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes. // 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 // Core shell precached on install so repeat opens are instant without any
// network wait. Keep this list in sync with the <script type="module"> tags // network wait. Keep this list in sync with the <script type="module"> tags