fix(dragdrop): unify tree+tab drop zone logic, fix cross-pane transfer
CI / lint (push) Failing after 9s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 18s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

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.
This commit is contained in:
2026-07-29 10:20:52 -04:00
parent 7389d26520
commit 380ca29f2c
2 changed files with 74 additions and 31 deletions
+3 -2
View File
@@ -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();
}
+71 -29
View File
@@ -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);