From 512de06903aa1d3c6efe8807e81970fddf7be7cb Mon Sep 17 00:00:00 2001 From: AlexsandrSnytkin Date: Fri, 18 Sep 2026 15:26:25 +0700 Subject: [PATCH] fix(ui): drag blocks by their shell --- tests/test_ui_server.py | 2 + ui_server.py | 88 ++++++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/tests/test_ui_server.py b/tests/test_ui_server.py index 6ab3458..8158d67 100644 --- a/tests/test_ui_server.py +++ b/tests/test_ui_server.py @@ -80,6 +80,8 @@ class UIServerTests(unittest.TestCase): self.assertIn('createCustomConfigBlock', HTML) self.assertIn('duplicateConfigBlock', HTML) self.assertIn('focusConfigBlock', HTML) + self.assertIn('startConfigBlockDrag', HTML) + self.assertIn('configDragBlocked', HTML) self.assertIn('positionNewConfigBlock', HTML) self.assertIn('finishConfigDrag', HTML) self.assertIn('fpv-config-layout-v1', HTML) diff --git a/ui_server.py b/ui_server.py index 4bc2d34..36e6f5e 100644 --- a/ui_server.py +++ b/ui_server.py @@ -1309,6 +1309,7 @@ HTML = """ .config-restore-panel[hidden], .config-trash[hidden] { display: none !important; } .config-canvas { position: relative; display: block; min-height: 760px; padding: 8px 6px 18px; overflow: auto; overscroll-behavior: contain; } .config-canvas > .config-block { position: absolute; box-sizing: border-box; container: config-block / inline-size; margin: 0; min-width: 240px; min-height: 110px; border: 1px solid var(--ui-border) !important; border-radius: 10px; background: var(--ui-panel) !important; box-shadow: 0 6px 18px rgba(0, 0, 0, .1); overflow: auto; transition: border-color .2s ease, box-shadow .2s ease, opacity .2s ease; } + .config-canvas > .config-block { cursor: grab; touch-action: none; } .config-canvas > .config-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)) !important; } .config-canvas > .config-block.is-dragging, .config-canvas > .config-block.is-resizing { z-index: 5; border-color: var(--ui-accent) !important; box-shadow: 0 10px 28px color-mix(in srgb, var(--ui-accent) 16%, transparent); transition: none; } .config-canvas > .config-custom-block { border-style: dashed !important; } @@ -1318,6 +1319,8 @@ HTML = """ .config-block > summary { cursor: grab; touch-action: none; } .config-block > summary:active, .config-block.is-dragging > summary { cursor: grabbing; } .config-block.is-dragging, .config-block.is-resizing { user-select: none; } + .config-block.is-dragging { cursor: grabbing; } + .config-block input, .config-block select, .config-block textarea, .config-block button, .config-block a { cursor: auto; touch-action: auto; } .config-block label, .config-block input, .config-block select, .config-block button, .config-block pre { min-width: 0; max-width: 100%; box-sizing: border-box; } .config-block .control-fields, .config-block .advanced-grid, .config-block .packet-grid, .config-block .packet-field-list { min-width: 0; } @container config-block (max-width: 620px) { @@ -2237,7 +2240,7 @@ HTML = """ block.addEventListener('pointerdown', event => { bringConfigBlockFront(block); const edge = configResizeEdge(event, block); - if (!edge || (event.target.closest('summary') && !edge.includes('n'))) return; + if (!edge || configDragBlocked(block, event)) return; event.preventDefault(); event.stopPropagation(); const startX = event.clientX; @@ -2290,41 +2293,9 @@ HTML = """ } }); block.addEventListener('pointerleave', () => { if (!block.classList.contains('is-resizing')) block.style.cursor = ''; }); - summary.addEventListener('pointerdown', event => { - if (event.button !== 0 || !configCanvas) return; - const startX = event.clientX; - const startY = event.clientY; - const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0; - const startTop = parseFloat(block.style.top) || block.offsetTop || 0; - let dragged = false; - summary.setPointerCapture?.(event.pointerId); - const move = moveEvent => { - if (!dragged && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 4) return; - if (!dragged) { - dragged = true; - event.preventDefault(); - block.classList.add('is-dragging'); - const trash = $('configTrash'); - if (trash) { trash.hidden = false; trash.classList.remove('is-over'); } - } - event.preventDefault(); - const maxLeft = Math.max(0, configCanvasWidth() - block.offsetWidth); - block.style.left = `${Math.max(0, Math.min(maxLeft, Math.round(startLeft + moveEvent.clientX - startX)))}px`; - block.style.top = `${Math.max(0, Math.round(startTop + moveEvent.clientY - startY))}px`; - setTrashHover(moveEvent); - updateConfigCanvasHeight(); - }; - const end = endEvent => { - summary.releasePointerCapture?.(event.pointerId); - summary.removeEventListener('pointermove', move); - summary.removeEventListener('pointerup', end); - summary.removeEventListener('pointercancel', end); - block.classList.remove('is-dragging'); - finishConfigDrag(block, endEvent, dragged); - }; - summary.addEventListener('pointermove', move); - summary.addEventListener('pointerup', end, {once: true}); - summary.addEventListener('pointercancel', end, {once: true}); + block.addEventListener('pointerdown', event => { + if (event.defaultPrevented || block.classList.contains('is-resizing')) return; + startConfigBlockDrag(block, event); }); block.addEventListener('toggle', () => { if (!block.open) block.open = true; @@ -2332,6 +2303,51 @@ HTML = """ updateConfigCanvasHeight(); }); } + function configDragBlocked(block, event) { + const target = event.target; + if (!(target instanceof Element)) return true; + const summary = block.firstElementChild; + const nestedSummary = target.closest('summary'); + if (nestedSummary && nestedSummary !== summary) return true; + if (nestedSummary === summary) return false; + return !!target.closest('input, select, textarea, button, a, [contenteditable="true"], .drop-zone, .packet-byte-map, .packet-field-row'); + } + function startConfigBlockDrag(block, event) { + if (event.button !== 0 || !configCanvas || configDragBlocked(block, event)) return; + const startX = event.clientX; + const startY = event.clientY; + const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0; + const startTop = parseFloat(block.style.top) || block.offsetTop || 0; + let dragged = false; + block.setPointerCapture?.(event.pointerId); + const move = moveEvent => { + if (!dragged && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 4) return; + if (!dragged) { + dragged = true; + event.preventDefault(); + block.classList.add('is-dragging'); + const trash = $('configTrash'); + if (trash) { trash.hidden = false; trash.classList.remove('is-over'); } + } + event.preventDefault(); + const maxLeft = Math.max(0, configCanvasWidth() - block.offsetWidth); + block.style.left = `${Math.max(0, Math.min(maxLeft, Math.round(startLeft + moveEvent.clientX - startX)))}px`; + block.style.top = `${Math.max(0, Math.round(startTop + moveEvent.clientY - startY))}px`; + setTrashHover(moveEvent); + updateConfigCanvasHeight(); + }; + const end = endEvent => { + block.releasePointerCapture?.(event.pointerId); + block.removeEventListener('pointermove', move); + block.removeEventListener('pointerup', end); + block.removeEventListener('pointercancel', end); + block.classList.remove('is-dragging'); + finishConfigDrag(block, endEvent, dragged); + }; + block.addEventListener('pointermove', move); + block.addEventListener('pointerup', end, {once: true}); + block.addEventListener('pointercancel', end, {once: true}); + } function resetConfigLayout() { const custom = {}; configBlocks.forEach(block => {