feat: #75 complété — 5 dernières sous-tâches implémentées
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 12s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
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 12s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
C1b: Sous-menu « Déplacer vers... » (flyout hover) — liste les autres panneaux
D2: Drag depuis l'arborescence vers panneau — format 'tree:{vault,path}'
E3: Rendu paresseux — display:none sur contenu des panneaux inactifs
E4: Verrouillage éditeur — même fichier déjà ouvert → focus le panneau existant
G3: Bouton reset (🔄 Réinitialiser les panneaux) dans la palette
ROADMAP: section 75 marquée ✅ Complété (reste I2/I3 tests)
This commit is contained in:
+9
-9
@@ -336,9 +336,9 @@
|
||||
|
||||
---
|
||||
|
||||
### 75. Éditeur multi-panneaux (Split View) ✅ 80% complété
|
||||
- **Effort :** 5-7 jours (4.5 jours réalisés) | **Impact :** 🟡
|
||||
- **Statut :** Fonctionnel — split right/down, drag & drop cross-pane, drag-to-split, navigation clavier, palette
|
||||
### 75. Éditeur multi-panneaux (Split View) ✅ Complété
|
||||
- **Effort :** 5-7 jours (réalisé) | **Impact :** 🟡
|
||||
- **Statut :** Fonctionnel — toutes les sous-tâches implémentées (reste tests I2/I3)
|
||||
- **Fichiers clés :** `frontend/js/pane-manager.js` (1082 loc), `frontend/js/viewer.js` (modifié), `frontend/js/ui.js` (modifié), `frontend/js/dashboard.js` (modifié), `frontend/js/palette.js` (modifié), `frontend/style.css` (modifié), `tests/test_pane_manager.py` (22 tests)
|
||||
- **Description :** Système de panneaux divisés permettant d'afficher plusieurs documents côte à côte ou superposés, à la manière d'un éditeur de code (VS Code). Drag & drop des onglets entre les panneaux, drag-to-split, menu contextuel, raccourcis clavier, palette de commandes.
|
||||
- **Architecture :**
|
||||
@@ -400,12 +400,12 @@
|
||||
- [ ] **I2.** Tests d'intégration frontend (JS DOM)
|
||||
- [ ] **I3.** Tests E2E Playwright (#58)
|
||||
|
||||
##### Reste à faire (~1-1.5 jours)
|
||||
- [ ] **C1b.** Sous-menu « Déplacer vers... » dans le menu contextuel des onglets
|
||||
- [ ] **D2.** Drag & drop depuis l'arborescence vers un panneau spécifique
|
||||
- [ ] **E3.** Rendu paresseux (mémoire)
|
||||
- [ ] **E4.** Verrouillage éditeur multi-panneau
|
||||
- [ ] **G3.** Bouton reset dans les paramètres/config
|
||||
##### Reste à faire (0)
|
||||
- [x] **C1b.** Sous-menu « Déplacer vers... » dans le menu contextuel des onglets
|
||||
- [x] **D2.** Drag & drop depuis l'arborescence vers un panneau spécifique
|
||||
- [x] **E3.** Rendu paresseux (mémoire) — contenu masqué via `display:none` sur panneaux inactifs
|
||||
- [x] **E4.** Verrouillage éditeur multi-panneau — même fichier ouvert dans 2 panneaux → focus le panneau existant
|
||||
- [x] **G3.** Bouton reset dans la palette de commandes (🔄 Réinitialiser les panneaux)
|
||||
- [ ] **I2.** Tests d'intégration frontend (JSDOM ou similaire)
|
||||
- [ ] **I3.** Tests E2E Playwright
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -155,7 +155,8 @@ function onDragStart(e) {
|
||||
|
||||
// Set drag data and visual feedback
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', path);
|
||||
// Prefix with 'tree:' so pane drop zones can distinguish from tab drags
|
||||
e.dataTransfer.setData('text/plain', 'tree:' + JSON.stringify({ vault, path }));
|
||||
|
||||
// Visual: reduce opacity of source
|
||||
treeItem.classList.add(DRAG_SOURCE_CLASS);
|
||||
|
||||
@@ -57,6 +57,7 @@ function getCMDS() {
|
||||
{ id:'close-pane', label:'✕ Fermer le panneau actif', desc:'Ferme le panneau actif et tous ses onglets', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit())window.PaneManager.closeActivePane();} },
|
||||
{ id:'focus-next', label:'→ Panneau suivant', desc:'Active le panneau suivant (Ctrl+Alt+→)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId+1)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
||||
{ id:'focus-prev', label:'← Panneau précédent', desc:'Active le panneau précédent (Ctrl+Alt+←)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId-1+window.PaneManager.panes.length)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} },
|
||||
{ id:'reset-panes', label:'🔄 Réinitialiser les panneaux', desc:'Ferme tous les panneaux et revient au mode single-pane', cat:'Panneaux', act:()=>{close();if(window.PaneManager){while(window.PaneManager.isSplit()){window.PaneManager.closePane(window.PaneManager.panes.length-1);}localStorage.removeItem('obsigate-panes');}} },
|
||||
];
|
||||
return _cmdsCache;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,18 @@ function createPaneTabManager(paneId) {
|
||||
this.activate(tabId);
|
||||
return;
|
||||
}
|
||||
// Check if same file is open in another pane (editor lock)
|
||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
||||
for (const [pid, tm] of Object.entries(window.PaneManager.tabManagers)) {
|
||||
if (Number(pid) === this.paneId) continue;
|
||||
if (tm._tabs.find(t => t.id === tabId)) {
|
||||
// File already open elsewhere — focus that pane instead
|
||||
window.PaneManager.setActivePane(Number(pid));
|
||||
tm.activate(tabId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const name = path.split('/').pop().replace(/\.md$/i, '');
|
||||
const icon = getFileIcon(name + '.md');
|
||||
this._tabs.push({ id: tabId, vault, path, name, icon });
|
||||
@@ -459,7 +471,18 @@ function createPaneTabManager(paneId) {
|
||||
menu.className = 'context-menu active';
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
const isSplit = window.PaneManager && window.PaneManager.isSplit();
|
||||
const pm = window.PaneManager;
|
||||
const isSplit = pm && pm.isSplit();
|
||||
|
||||
// Build "Déplacer vers..." submenu items
|
||||
let moveToHtml = '';
|
||||
if (isSplit) {
|
||||
const otherPanes = pm.panes.filter(p => p.id !== this.paneId);
|
||||
if (otherPanes.length > 0) {
|
||||
moveToHtml = '<div class="context-menu-separator"></div><div class="context-menu-item context-menu-sub" data-action="moveTo"><i data-lucide="arrow-right-left" class="icon"></i><span>Déplacer vers...</span><span class="context-menu-arrow">▸</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
let closePaneHtml = '';
|
||||
if (isSplit) {
|
||||
closePaneHtml = `<div class="context-menu-separator"></div><div class="context-menu-item" data-action="closePane"><i data-lucide="panel-right-close" class="icon"></i><span>Fermer le panneau</span></div>`;
|
||||
@@ -473,23 +496,59 @@ function createPaneTabManager(paneId) {
|
||||
<div class="context-menu-item" data-action="closeRight"><i data-lucide="arrow-right-circle" class="icon"></i><span>Fermer à droite</span></div>
|
||||
<div class="context-menu-separator"></div>
|
||||
<div class="context-menu-item" data-action="closeAll"><i data-lucide="trash-2" class="icon"></i><span>Fermer tout</span></div>
|
||||
${moveToHtml}
|
||||
${closePaneHtml}
|
||||
`;
|
||||
document.body.appendChild(menu);
|
||||
safeCreateIcons();
|
||||
|
||||
// "Déplacer vers..." flyout submenu
|
||||
const moveToEl = menu.querySelector('[data-action="moveTo"]');
|
||||
if (moveToEl && isSplit) {
|
||||
const otherPanes = pm.panes.filter(p => p.id !== this.paneId);
|
||||
let submenu = null;
|
||||
|
||||
moveToEl.addEventListener('mouseenter', () => {
|
||||
if (submenu) submenu.remove();
|
||||
submenu = document.createElement('div');
|
||||
submenu.className = 'context-menu active context-submenu';
|
||||
const parentRect = moveToEl.getBoundingClientRect();
|
||||
submenu.style.left = (parentRect.right + 4) + 'px';
|
||||
submenu.style.top = parentRect.top + 'px';
|
||||
submenu.innerHTML = otherPanes.map((p, i) =>
|
||||
`<div class="context-menu-item" data-pane="${p.id}"><i data-lucide="panel-right" class="icon"></i><span>Panneau ${p.id + 1}</span></div>`
|
||||
).join('');
|
||||
document.body.appendChild(submenu);
|
||||
safeCreateIcons();
|
||||
submenu.addEventListener('click', (se) => {
|
||||
const targetPane = parseInt(se.target.closest('.context-menu-item')?.dataset.pane);
|
||||
if (!isNaN(targetPane)) {
|
||||
pm.transferTab(this.paneId, tabId, targetPane);
|
||||
menu.remove(); submenu.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
moveToEl.addEventListener('mouseleave', (e) => {
|
||||
setTimeout(() => {
|
||||
if (submenu && !submenu.matches(':hover')) { submenu.remove(); submenu = null; }
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
menu.addEventListener('click', (e) => {
|
||||
const action = e.target.closest('.context-menu-item')?.dataset.action;
|
||||
if (action === 'splitRight') { if (window.PaneManager) { window.PaneManager.splitRight(); menu.remove(); return; } }
|
||||
else if (action === 'splitDown') { if (window.PaneManager) { window.PaneManager.splitDown(); menu.remove(); return; } }
|
||||
else if (action === 'closePane') { if (window.PaneManager) { window.PaneManager.closeActivePane(); menu.remove(); return; } }
|
||||
if (action === 'moveTo') return; // handled by hover
|
||||
if (action === 'splitRight') { if (pm) { pm.splitRight(); menu.remove(); return; } }
|
||||
else if (action === 'splitDown') { if (pm) { pm.splitDown(); menu.remove(); return; } }
|
||||
else if (action === 'closePane') { if (pm) { pm.closeActivePane(); menu.remove(); return; } }
|
||||
else if (action === 'close') this.close(tabId);
|
||||
else if (action === 'closeOthers') this.closeOthers(tabId);
|
||||
else if (action === 'closeRight') this.closeRight(tabId);
|
||||
else if (action === 'closeAll') this.closeAll();
|
||||
menu.remove();
|
||||
});
|
||||
const closeMenu = () => menu.remove();
|
||||
const closeMenu = () => { menu.remove(); };
|
||||
document.addEventListener('click', closeMenu, { once: true });
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') menu.remove(); }, { once: true });
|
||||
},
|
||||
@@ -554,7 +613,17 @@ const PaneManager = {
|
||||
|
||||
setActivePane(paneId) {
|
||||
if (this.activePaneId === paneId) return;
|
||||
// Hide previous active pane content (lazy rendering)
|
||||
if (this.panes[this.activePaneId] && this.panes[this.activePaneId].element) {
|
||||
const oldContent = this.panes[this.activePaneId].element.querySelector('.content-area');
|
||||
if (oldContent) oldContent.style.display = 'none';
|
||||
}
|
||||
this.activePaneId = paneId;
|
||||
// Show new active pane content
|
||||
if (this.panes[paneId] && this.panes[paneId].element) {
|
||||
const newContent = this.panes[paneId].element.querySelector('.content-area');
|
||||
if (newContent) newContent.style.display = '';
|
||||
}
|
||||
this._highlightActive();
|
||||
// Sync global state from this pane's active tab
|
||||
const tm = this.tabManagers[paneId];
|
||||
@@ -856,6 +925,24 @@ const PaneManager = {
|
||||
console.log('[PaneManager] drop fired, raw data:', raw ? raw.substring(0, 80) : 'null');
|
||||
|
||||
if (!raw) return;
|
||||
|
||||
// Handle 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 (tm) {
|
||||
window.PaneManager.setActivePane(paneId);
|
||||
tm.openPreview(treeData.vault, treeData.path);
|
||||
}
|
||||
}
|
||||
} catch (e2) { console.debug('[PaneManager] tree drop parse error:', e2); }
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle 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; }
|
||||
|
||||
Reference in New Issue
Block a user