From 380ca29f2c80e08aeee4917be5cbc706c95158ef Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 29 Jul 2026 10:20:52 -0400 Subject: [PATCH] fix(dragdrop): unify tree+tab drop zone logic, fix cross-pane transfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes for drag-and-drop in split view (#75): 1. Tree drag-to-split: edge zone detection now runs BEFORE the tree/tab dispatch, so dragging a sidebar file to the right/bottom edge of a single pane correctly creates a split and opens the file in the new pane (was: tree check returned early before edge zone logic). 2. Cross-pane transfer consistency: transferTab() now tracks pane count before the transfer — if closePane() fires inside transferTabTo() (source pane becomes empty), the DOM is rebuilt and the original targetTm reference is stale. After close, re-lookup the tab manager that actually holds the transferred tab before _renderTabs()+activate. 3. dragdrop.js flicker: onDragLeave() now checks relatedTarget instead of unconditionally clearing highlights on every child boundary. --- frontend/js/dragdrop.js | 5 +- frontend/js/pane-manager.js | 100 +++++++++++++++++++++++++----------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/frontend/js/dragdrop.js b/frontend/js/dragdrop.js index ab9acfc..e5cbb1c 100644 --- a/frontend/js/dragdrop.js +++ b/frontend/js/dragdrop.js @@ -196,10 +196,11 @@ function onDragOver(e) { } function onDragLeave(e) { - // Only clear when actually leaving the container, not entering a child + // Only clear when actually leaving the container, not entering a child. + // Use relatedTarget: if it's still inside #vault-tree, keep highlights. const container = document.getElementById('vault-tree'); if (!container) return; - // Use a small delay to avoid flickering when moving between children + if (e.relatedTarget && container.contains(e.relatedTarget)) return; clearAllHighlights(); } diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js index 968d62c..98da2a4 100644 --- a/frontend/js/pane-manager.js +++ b/frontend/js/pane-manager.js @@ -659,12 +659,29 @@ const PaneManager = { const fromTm = this.tabManagers[fromPaneId]; const toTm = this.tabManagers[toPaneId]; if (!fromTm || !toTm) return false; + + // Track state BEFORE transfer (closePane may rebuild DOM) + const paneCountBefore = this.panes.length; + const ok = fromTm.transferTabTo(tabId, toTm); - if (ok) { - toTm._renderTabs(); - toTm.activate(tabId); - this._persist(); + if (!ok) return false; + + // If source pane was auto-closed, DOM was rebuilt — re-lookup target + let actualTm = toTm; + if (this.panes.length < paneCountBefore) { + // Find which pane manager now holds the tab + for (const [pid, tm] of Object.entries(this.tabManagers)) { + if (tm._tabs.find(t => t.id === tabId)) { + actualTm = tm; + console.log('[PaneManager] source closed, tab now in pane', pid); + break; + } + } } + + actualTm._renderTabs(); + actualTm.activate(tabId); + this._persist(); return ok; }, @@ -1060,15 +1077,53 @@ const PaneManager = { if (!raw) return; - // Handle tree drag (file from sidebar) + // --- Determine drop zone --- + const rect = container.getBoundingClientRect(); + const relX = e.clientX - rect.left; + const relY = e.clientY - rect.top; + const isSingle = !this.isSplit(); + const onRightEdge = isSingle && relX > rect.width * 0.72; + const onBottomEdge = isSingle && relY > rect.height * 0.72; + + console.log('[PaneManager] drop at', relX.toFixed(0), relY.toFixed(0), + 'rect', rect.width.toFixed(0), 'x', rect.height.toFixed(0), + 'isSingle', isSingle, 'paneId', paneId, + 'onRightEdge', onRightEdge, 'onBottomEdge', onBottomEdge); + + // --- Tree drag (file from sidebar) --- if (raw.startsWith('tree:')) { try { const treeData = JSON.parse(raw.substring(5)); - if (treeData.vault && treeData.path) { - console.log('[PaneManager] tree drop → opening in pane', paneId); - const tm = window.PaneManager.getTabManager(paneId); + if (!treeData.vault || !treeData.path) return; + console.log('[PaneManager] tree drop vault=', treeData.vault, 'path=', treeData.path); + + if (onRightEdge) { + console.log('[PaneManager] → tree splitRight'); + this.splitRight(); + setTimeout(() => { + const newPaneId = this.panes.length - 1; + const tm = this.getTabManager(newPaneId); + if (tm) { + this.setActivePane(newPaneId); + tm.openPreview(treeData.vault, treeData.path); + } + }, 200); + } else if (onBottomEdge) { + console.log('[PaneManager] → tree splitDown'); + this.splitDown(); + setTimeout(() => { + const newPaneId = this.panes.length - 1; + const tm = this.getTabManager(newPaneId); + if (tm) { + this.setActivePane(newPaneId); + tm.openPreview(treeData.vault, treeData.path); + } + }, 200); + } else { + console.log('[PaneManager] → tree open in pane', paneId); + const tm = this.getTabManager(paneId); if (tm) { - window.PaneManager.setActivePane(paneId); + this.setActivePane(paneId); tm.openPreview(treeData.vault, treeData.path); } } @@ -1076,39 +1131,26 @@ const PaneManager = { return; } - // Handle tab drag + // --- Tab drag --- let dragData; try { dragData = JSON.parse(raw); } catch (e2) { console.log('[PaneManager] JSON parse error:', e2); return; } if (dragData.paneId == null || dragData.tabId == null) { console.log('[PaneManager] missing paneId/tabId'); return; } - // Recalculate edge zone from drop coordinates - const rect = container.getBoundingClientRect(); - const relX = e.clientX - rect.left; - const relY = e.clientY - rect.top; - const isSingle = !this.isSplit(); - - console.log('[PaneManager] drop at', relX.toFixed(0), relY.toFixed(0), - 'rect', rect.width.toFixed(0), 'x', rect.height.toFixed(0), - 'isSingle', isSingle, 'paneId', paneId, 'srcPane', dragData.paneId); - - if (isSingle && relX > rect.width * 0.72) { + if (onRightEdge) { console.log('[PaneManager] → splitRight'); this.splitRight(); - // Transfer after DOM settles - const transfer = () => { + setTimeout(() => { const newPaneId = this.panes.length - 1; console.log('[PaneManager] transferring tab', dragData.tabId, 'to pane', newPaneId); this.transferTab(dragData.paneId, dragData.tabId, newPaneId); - }; - setTimeout(transfer, 200); - } else if (isSingle && relY > rect.height * 0.72) { + }, 200); + } else if (onBottomEdge) { console.log('[PaneManager] → splitDown'); this.splitDown(); - const transfer = () => { + setTimeout(() => { const newPaneId = this.panes.length - 1; this.transferTab(dragData.paneId, dragData.tabId, newPaneId); - }; - setTimeout(transfer, 200); + }, 200); } else if (dragData.paneId !== paneId) { console.log('[PaneManager] → cross-pane transfer', dragData.paneId, '→', paneId); this.transferTab(dragData.paneId, dragData.tabId, paneId);