diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index 5de6b45..aabec7f 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -414,6 +414,7 @@
Menu
Find & Replace
+
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 559ab6a..cf003b0 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -3915,51 +3915,107 @@ This is a fully client-side application. Your content never leaves your browser let isFrDocked = false; let dragOffset = { x: 0, y: 0 }; let isPanelDragging = false; + let lastFloatingLeft = null; + let lastFloatingTop = null; + let lastFloatingRight = null; function initFindReplacePanelDrag() { const handle = document.getElementById('find-replace-drag-handle'); const panel = document.getElementById('find-replace-modal'); if (!handle || !panel) return; - handle.addEventListener('mousedown', (e) => { - if (isFrDocked) return; - if (e.target.closest('.find-replace-header-actions')) return; + const startDrag = (clientX, clientY) => { isPanelDragging = true; - dragOffset.x = e.clientX - panel.offsetLeft; - dragOffset.y = e.clientY - panel.offsetTop; + dragOffset.x = clientX - panel.offsetLeft; + dragOffset.y = clientY - panel.offsetTop; document.body.classList.add('resizing'); - }); + }; - document.addEventListener('mousemove', (e) => { - if (!isPanelDragging || isFrDocked) return; - const x = e.clientX - dragOffset.x; - const y = e.clientY - dragOffset.y; + const moveDrag = (clientX, clientY) => { + const x = clientX - dragOffset.x; + const y = clientY - dragOffset.y; // Keep panel inside viewport boundaries const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; - panel.style.left = `${Math.max(0, Math.min(maxX, x))}px`; - panel.style.top = `${Math.max(0, Math.min(maxY, y))}px`; + const newLeft = `${Math.max(0, Math.min(maxX, x))}px`; + const newTop = `${Math.max(0, Math.min(maxY, y))}px`; + panel.style.left = newLeft; + panel.style.top = newTop; panel.style.right = 'auto'; - }); - document.addEventListener('mouseup', () => { + lastFloatingLeft = newLeft; + lastFloatingTop = newTop; + lastFloatingRight = 'auto'; + }; + + const stopDrag = () => { if (isPanelDragging) { isPanelDragging = false; document.body.classList.remove('resizing'); } + }; + + // Mouse events + handle.addEventListener('mousedown', (e) => { + if (isFrDocked) return; + if (window.innerWidth < 768) return; // Do NOT allow dragging on mobile layouts + if (e.target.closest('.find-replace-header-actions')) return; + startDrag(e.clientX, e.clientY); }); + + document.addEventListener('mousemove', (e) => { + if (!isPanelDragging || isFrDocked) return; + moveDrag(e.clientX, e.clientY); + }); + + document.addEventListener('mouseup', stopDrag); + + // Touch events for tablets + handle.addEventListener('touchstart', (e) => { + if (isFrDocked) return; + if (window.innerWidth < 768) return; // Do NOT allow dragging on mobile layouts + if (e.target.closest('.find-replace-header-actions')) return; + if (e.touches && e.touches[0]) { + startDrag(e.touches[0].clientX, e.touches[0].clientY); + } + }, { passive: true }); + + document.addEventListener('touchmove', (e) => { + if (!isPanelDragging || isFrDocked) return; + if (e.touches && e.touches[0]) { + moveDrag(e.touches[0].clientX, e.touches[0].clientY); + } + }, { passive: true }); + + document.addEventListener('touchend', stopDrag); } let frPreferredDocked = false; function toggleFrDockMode(forceFloat = false) { + // If forceFloat is an Event (e.g. from click listener directly), treat as false + if (forceFloat instanceof Event || (forceFloat && typeof forceFloat === 'object')) { + forceFloat = false; + } + const panel = document.getElementById('find-replace-modal'); const dockBtn = document.getElementById('find-replace-dock'); const contentCont = document.querySelector('.content-container'); if (!panel || !dockBtn || !contentCont) return; - if (window.innerWidth <= 768 || forceFloat) { + // Save active element focus and selection before DOM movement + const activeEl = document.activeElement; + const activeId = activeEl ? activeEl.id : null; + const isInput = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'SELECT' || activeEl.tagName === 'TEXTAREA'); + let selStart = 0; + let selEnd = 0; + if (isInput && typeof activeEl.selectionStart === 'number') { + selStart = activeEl.selectionStart; + selEnd = activeEl.selectionEnd; + } + + if (window.innerWidth < 1080 || forceFloat) { isFrDocked = false; panel.classList.remove('docked'); if (panel.parentElement !== document.body) { @@ -3968,15 +4024,26 @@ This is a fully client-side application. Your content never leaves your browser contentCont.classList.remove('fr-docked'); contentCont.style.setProperty('--dock-width', '0px'); - panel.style.top = ''; - panel.style.left = ''; - panel.style.right = ''; + panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : ''; + panel.style.top = lastFloatingTop !== null ? lastFloatingTop : ''; + panel.style.right = lastFloatingRight !== null ? lastFloatingRight : ''; dockBtn.innerHTML = ''; dockBtn.title = "Toggle Dock Mode"; panel.style.display = 'flex'; applyPaneWidths(); + + // Restore focus and selection + if (activeId) { + const el = document.getElementById(activeId); + if (el) { + el.focus(); + if (isInput && typeof el.selectionStart === 'number') { + el.setSelectionRange(selStart, selEnd); + } + } + } return; } @@ -4007,9 +4074,9 @@ This is a fully client-side application. Your content never leaves your browser contentCont.classList.remove('fr-docked'); contentCont.style.setProperty('--dock-width', '0px'); - panel.style.top = ''; - panel.style.left = ''; - panel.style.right = ''; + panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : ''; + panel.style.top = lastFloatingTop !== null ? lastFloatingTop : ''; + panel.style.right = lastFloatingRight !== null ? lastFloatingRight : ''; dockBtn.innerHTML = ''; dockBtn.title = "Toggle Dock Mode"; @@ -4018,6 +4085,17 @@ This is a fully client-side application. Your content never leaves your browser // Ensure display is flex and recalculate split panes panel.style.display = 'flex'; applyPaneWidths(); + + // Restore focus and selection after layout change + if (activeId) { + const el = document.getElementById(activeId); + if (el) { + el.focus(); + if (isInput && typeof el.selectionStart === 'number') { + el.setSelectionRange(selStart, selEnd); + } + } + } } function updateFindControls() { @@ -4143,7 +4221,7 @@ This is a fully client-side application. Your content never leaves your browser let wasDockedPref = localStorage.getItem('find-replace-docked') === 'true'; // Force floating-only mode on mobile/tablet viewports - if (window.innerWidth <= 768) { + if (window.innerWidth < 1080) { wasDockedPref = false; } @@ -4380,10 +4458,23 @@ This is a fully client-side application. Your content never leaves your browser }); } + // Reset position handler + const resetBtn = document.getElementById('find-replace-reset'); + if (resetBtn) { + resetBtn.addEventListener('click', () => { + lastFloatingLeft = null; + lastFloatingTop = null; + lastFloatingRight = null; + modal.style.left = ''; + modal.style.top = ''; + modal.style.right = ''; + }); + } + // Dock toggle handler const dockBtn = document.getElementById('find-replace-dock'); if (dockBtn) { - dockBtn.addEventListener('click', toggleFrDockMode); + dockBtn.addEventListener('click', () => toggleFrDockMode(false)); } // Advanced Drawer Toggle @@ -4619,7 +4710,7 @@ This is a fully client-side application. Your content never leaves your browser } function startResize(e) { - if (window.innerWidth <= 768) return; + if (window.innerWidth < 1080) return; if (currentViewMode !== 'split') return; e.preventDefault(); isResizing = true; @@ -4628,7 +4719,7 @@ This is a fully client-side application. Your content never leaves your browser } function startResizeTouch(e) { - if (window.innerWidth <= 768) return; + if (window.innerWidth < 1080) return; if (currentViewMode !== 'split') return; e.preventDefault(); isResizing = true; @@ -4675,7 +4766,7 @@ This is a fully client-side application. Your content never leaves your browser } function applyPaneWidths() { - if (window.innerWidth <= 768) { + if (window.innerWidth < 1080) { resetPaneWidths(); return; } @@ -4787,11 +4878,36 @@ This is a fully client-side application. Your content never leaves your browser // Initialize resizer - Story 1.3 initResizer(); + function constrainFloatingPanelPosition() { + const panel = document.getElementById('find-replace-modal'); + if (!panel || isFrDocked || panel.style.display === 'none') return; + if (window.innerWidth < 768) return; // Mobile layout forces fixed responsive positioning via CSS + + // Only adjust if the inline style has custom dragged coordinates + if (!panel.style.left || panel.style.left === 'auto') return; + + const leftVal = parseFloat(panel.style.left) || 0; + const topVal = parseFloat(panel.style.top) || 0; + + const maxX = window.innerWidth - panel.offsetWidth; + const maxY = window.innerHeight - panel.offsetHeight; + + const constrainedLeft = `${Math.max(0, Math.min(maxX, leftVal))}px`; + const constrainedTop = `${Math.max(0, Math.min(maxY, topVal))}px`; + + panel.style.left = constrainedLeft; + panel.style.top = constrainedTop; + + lastFloatingLeft = constrainedLeft; + lastFloatingTop = constrainedTop; + } + window.addEventListener('resize', () => { scheduleLineNumberUpdate(); - if (window.innerWidth <= 768 && isFrDocked && isFindModalOpen) { + if (window.innerWidth < 1080 && isFrDocked && isFindModalOpen) { toggleFrDockMode(true); } + constrainFloatingPanelPosition(); }); // View Mode Button Event Listeners - Story 1.1 diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index ee4df05..e5f7569 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -3013,6 +3013,10 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang= flex-shrink: 0; } +.find-replace-panel.docked #find-replace-reset { + display: none !important; +} + .find-replace-header { display: flex; align-items: center; @@ -3379,13 +3383,15 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang= } /* ======================================== - MOBILE FIND PANEL RESPONSIVE FIXES + MOBILE & TABLET FIND PANEL RESPONSIVE FIXES ======================================== */ -@media (max-width: 768px) { +@media (max-width: 1079px) { #find-replace-dock { display: none !important; } - +} + +@media (max-width: 768px) { /* Prevent full screen expansion of floating panel on small mobile viewports */ .find-replace-panel { width: calc(100% - 24px) !important; diff --git a/index.html b/index.html index 73b66dc..b1449cd 100644 --- a/index.html +++ b/index.html @@ -411,6 +411,7 @@
Menu
Find & Replace
+
diff --git a/script.js b/script.js index dfec580..cf003b0 100644 --- a/script.js +++ b/script.js @@ -3915,6 +3915,9 @@ This is a fully client-side application. Your content never leaves your browser let isFrDocked = false; let dragOffset = { x: 0, y: 0 }; let isPanelDragging = false; + let lastFloatingLeft = null; + let lastFloatingTop = null; + let lastFloatingRight = null; function initFindReplacePanelDrag() { const handle = document.getElementById('find-replace-drag-handle'); @@ -3935,9 +3938,15 @@ This is a fully client-side application. Your content never leaves your browser // Keep panel inside viewport boundaries const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; - panel.style.left = `${Math.max(0, Math.min(maxX, x))}px`; - panel.style.top = `${Math.max(0, Math.min(maxY, y))}px`; + const newLeft = `${Math.max(0, Math.min(maxX, x))}px`; + const newTop = `${Math.max(0, Math.min(maxY, y))}px`; + panel.style.left = newLeft; + panel.style.top = newTop; panel.style.right = 'auto'; + + lastFloatingLeft = newLeft; + lastFloatingTop = newTop; + lastFloatingRight = 'auto'; }; const stopDrag = () => { @@ -4015,9 +4024,9 @@ This is a fully client-side application. Your content never leaves your browser contentCont.classList.remove('fr-docked'); contentCont.style.setProperty('--dock-width', '0px'); - panel.style.top = ''; - panel.style.left = ''; - panel.style.right = ''; + panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : ''; + panel.style.top = lastFloatingTop !== null ? lastFloatingTop : ''; + panel.style.right = lastFloatingRight !== null ? lastFloatingRight : ''; dockBtn.innerHTML = ''; dockBtn.title = "Toggle Dock Mode"; @@ -4065,9 +4074,9 @@ This is a fully client-side application. Your content never leaves your browser contentCont.classList.remove('fr-docked'); contentCont.style.setProperty('--dock-width', '0px'); - panel.style.top = ''; - panel.style.left = ''; - panel.style.right = ''; + panel.style.left = lastFloatingLeft !== null ? lastFloatingLeft : ''; + panel.style.top = lastFloatingTop !== null ? lastFloatingTop : ''; + panel.style.right = lastFloatingRight !== null ? lastFloatingRight : ''; dockBtn.innerHTML = ''; dockBtn.title = "Toggle Dock Mode"; @@ -4449,6 +4458,19 @@ This is a fully client-side application. Your content never leaves your browser }); } + // Reset position handler + const resetBtn = document.getElementById('find-replace-reset'); + if (resetBtn) { + resetBtn.addEventListener('click', () => { + lastFloatingLeft = null; + lastFloatingTop = null; + lastFloatingRight = null; + modal.style.left = ''; + modal.style.top = ''; + modal.style.right = ''; + }); + } + // Dock toggle handler const dockBtn = document.getElementById('find-replace-dock'); if (dockBtn) { @@ -4870,8 +4892,14 @@ This is a fully client-side application. Your content never leaves your browser const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; - panel.style.left = `${Math.max(0, Math.min(maxX, leftVal))}px`; - panel.style.top = `${Math.max(0, Math.min(maxY, topVal))}px`; + const constrainedLeft = `${Math.max(0, Math.min(maxX, leftVal))}px`; + const constrainedTop = `${Math.max(0, Math.min(maxY, topVal))}px`; + + panel.style.left = constrainedLeft; + panel.style.top = constrainedTop; + + lastFloatingLeft = constrainedLeft; + lastFloatingTop = constrainedTop; } window.addEventListener('resize', () => { diff --git a/styles.css b/styles.css index 5939487..e5f7569 100644 --- a/styles.css +++ b/styles.css @@ -3013,6 +3013,10 @@ html[lang="ko"] .markdown-body h1, html[lang="ko"] .markdown-body h2, html[lang= flex-shrink: 0; } +.find-replace-panel.docked #find-replace-reset { + display: none !important; +} + .find-replace-header { display: flex; align-items: center;