feat: drop cross-pane fonctionnel + drag-to-split (bord droit/bas)
CI / lint (push) Failing after 12s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 16s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Failing after 12s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 16s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
- _setupPaneDropZone: dragover/drop sur .pane-container (fonctionne même si tab bar vide) - Drag-to-split: déposer un onglet dans les 25% droit → split right, 25% bas → split down - Indicateurs visuels: zone bleue semi-transparente au drop, tab-bar dashed outline - stopPropagation dans le drop handler des tabs pour éviter double-fire - CSS: .pane-drop-zone-indicator, .tab-bar.drop-target, position:relative sur pane-container
This commit is contained in:
@@ -390,6 +390,7 @@ function createPaneTabManager(paneId) {
|
|||||||
});
|
});
|
||||||
el.addEventListener('drop', (e) => {
|
el.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); // Prevent container handler from double-firing
|
||||||
document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
|
document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
|
||||||
try {
|
try {
|
||||||
const raw = e.dataTransfer.getData('application/obsigate-tab');
|
const raw = e.dataTransfer.getData('application/obsigate-tab');
|
||||||
@@ -720,15 +721,99 @@ const PaneManager = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.addEventListener('click', (e) => {
|
container.addEventListener('click', (e) => {
|
||||||
// Don't capture clicks on tab items (they have their own handler)
|
|
||||||
if (e.target.closest('.tab-item')) return;
|
if (e.target.closest('.tab-item')) return;
|
||||||
this.setActivePane(idx);
|
this.setActivePane(idx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Drop zone handlers on the pane container ---
|
||||||
|
this._setupPaneDropZone(container, idx);
|
||||||
|
|
||||||
grid.appendChild(container);
|
grid.appendChild(container);
|
||||||
return { id: idx, element: container, width: null };
|
return { id: idx, element: container, width: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Add dragover/drop on a pane container for cross-pane transfers + drag-to-split */
|
||||||
|
_setupPaneDropZone(container, paneId) {
|
||||||
|
let _edgeZone = null; // 'right' | 'bottom' | null
|
||||||
|
|
||||||
|
container.addEventListener('dragover', (e) => {
|
||||||
|
const raw = e.dataTransfer.types.includes('application/obsigate-tab');
|
||||||
|
if (!raw) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
|
// Detect edge zones for drag-to-split (only in single-pane mode)
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const relX = e.clientX - rect.left;
|
||||||
|
const relY = e.clientY - rect.top;
|
||||||
|
const isSingle = !this.isSplit();
|
||||||
|
|
||||||
|
// Clean up previous indicators
|
||||||
|
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
|
||||||
|
_edgeZone = null;
|
||||||
|
|
||||||
|
if (isSingle) {
|
||||||
|
if (relX > rect.width * 0.75) {
|
||||||
|
_edgeZone = 'right';
|
||||||
|
} else if (relY > rect.height * 0.75) {
|
||||||
|
_edgeZone = 'bottom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_edgeZone) {
|
||||||
|
// Show edge zone indicator
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'pane-drop-zone-indicator pane-drop-zone-' + _edgeZone;
|
||||||
|
container.appendChild(indicator);
|
||||||
|
} else {
|
||||||
|
// Show tab-bar highlight for cross-pane transfer
|
||||||
|
const tabBar = container.querySelector('.tab-bar');
|
||||||
|
if (tabBar) tabBar.classList.add('drop-target');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('dragleave', (e) => {
|
||||||
|
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
|
||||||
|
const tabBar = container.querySelector('.tab-bar');
|
||||||
|
if (tabBar) tabBar.classList.remove('drop-target');
|
||||||
|
_edgeZone = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
|
||||||
|
const tabBar = container.querySelector('.tab-bar');
|
||||||
|
if (tabBar) tabBar.classList.remove('drop-target');
|
||||||
|
|
||||||
|
const raw = e.dataTransfer.getData('application/obsigate-tab');
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const dragData = JSON.parse(raw);
|
||||||
|
|
||||||
|
if (_edgeZone === 'right') {
|
||||||
|
// Drag to right edge → split right with this tab
|
||||||
|
this.splitRight();
|
||||||
|
// Transfer the dragged tab to the new pane
|
||||||
|
setTimeout(() => {
|
||||||
|
const newPaneId = this.panes.length - 1;
|
||||||
|
this.transferTab(dragData.paneId, dragData.tabId, newPaneId);
|
||||||
|
}, 100);
|
||||||
|
} else if (_edgeZone === 'bottom') {
|
||||||
|
// Drag to bottom edge → split down with this tab
|
||||||
|
this.splitDown();
|
||||||
|
setTimeout(() => {
|
||||||
|
const newPaneId = this.panes.length - 1;
|
||||||
|
this.transferTab(dragData.paneId, dragData.tabId, newPaneId);
|
||||||
|
}, 100);
|
||||||
|
} else if (dragData.paneId !== paneId) {
|
||||||
|
// Cross-pane transfer (dropped on tab bar or content area of another pane)
|
||||||
|
this.transferTab(dragData.paneId, dragData.tabId, paneId);
|
||||||
|
}
|
||||||
|
} catch (e2) { /* ignore */ }
|
||||||
|
_edgeZone = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_addPane() {
|
_addPane() {
|
||||||
const grid = document.querySelector('.pane-grid');
|
const grid = document.querySelector('.pane-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|||||||
@@ -1264,6 +1264,7 @@ select {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
transition: border-color 150ms ease;
|
transition: border-color 150ms ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-container.pane-active {
|
.pane-container.pane-active {
|
||||||
@@ -1317,6 +1318,38 @@ select {
|
|||||||
border: 2px solid var(--accent);
|
border: 2px solid var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Drop zone indicators for drag-to-split --- */
|
||||||
|
.pane-drop-zone-indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-drop-zone-indicator.pane-drop-zone-right {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 25%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-drop-zone-indicator.pane-drop-zone-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab bar drop target highlight */
|
||||||
|
.tab-bar.drop-target {
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0.15;
|
||||||
|
outline: 2px dashed var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive: disable split view below 768px */
|
/* Responsive: disable split view below 768px */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pane-grid {
|
.pane-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user