feat: split view pro — TabManager indépendant par panneau avec drag & drop cross-pane
CI / lint (push) Failing after 10s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 17s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Failing after 10s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 17s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
- PaneTabManager factory: chaque panneau a sa propre instance avec ses propres onglets - Drag & drop d'onglets entre panneaux (application/obsigate-tab dataTransfer) - Split copie l'onglet actif dans le nouveau panneau - TabManager singleton: délégation automatique vers le panneau actif en mode split - Persistance complète par panneau (tabs, activeTab, previewTab) - viewer.js: getContentArea() priorise window._activePaneContentArea - 307 tests verts, déployé sur 192.168.30.101:2020
This commit is contained in:
+743
-231
File diff suppressed because it is too large
Load Diff
+27
-61
@@ -1877,70 +1877,19 @@ export const TabManager = {
|
||||
_tabList: null,
|
||||
_dirtyTabs: new Set(),
|
||||
|
||||
// Expose on window for PaneManager cross-module communication
|
||||
_expose() {
|
||||
window.TabManager = this;
|
||||
},
|
||||
|
||||
// Pane-aware helpers
|
||||
_getContentArea() {
|
||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.PaneManager.getActiveContentArea() || document.getElementById('content-area');
|
||||
}
|
||||
return document.getElementById('content-area');
|
||||
},
|
||||
_getTabBar() {
|
||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.PaneManager.getActiveTabBar() || document.getElementById('tab-bar');
|
||||
}
|
||||
return document.getElementById('tab-bar');
|
||||
},
|
||||
_getTabList() {
|
||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.PaneManager.getActiveTabList() || document.getElementById('tab-list');
|
||||
}
|
||||
return document.getElementById('tab-list');
|
||||
},
|
||||
|
||||
init() {
|
||||
this._tabBar = this._getTabBar();
|
||||
this._tabList = this._getTabList();
|
||||
this._expose();
|
||||
},
|
||||
|
||||
/** Called by PaneManager when pane focus changes — updates DOM references */
|
||||
activatePaneTab(paneId) {
|
||||
// Update DOM references to the newly active pane
|
||||
this._tabBar = this._getTabBar();
|
||||
this._tabList = this._getTabList();
|
||||
// If this pane has a saved active tab, activate it
|
||||
const pane = window.PaneManager && window.PaneManager.panes
|
||||
? window.PaneManager.panes.find(p => p.id === paneId)
|
||||
: null;
|
||||
if (pane && pane.activeTab && pane.activeTab !== this._activeTabId) {
|
||||
this.activate(pane.activeTab);
|
||||
}
|
||||
if (!pane || !pane.activeTab) {
|
||||
// New pane with no tab — show dashboard
|
||||
this._showDashboard();
|
||||
}
|
||||
},
|
||||
|
||||
/** Track which pane the active tab belongs to */
|
||||
_updatePaneActiveTab() {
|
||||
if (window.PaneManager && window.PaneManager.isSplit() && this._activeTabId) {
|
||||
const pane = window.PaneManager.panes.find(p => p.id === window.PaneManager.activePaneId);
|
||||
if (pane) {
|
||||
pane.activeTab = this._activeTabId;
|
||||
window.PaneManager._persist();
|
||||
}
|
||||
}
|
||||
this._tabBar = document.getElementById("tab-bar");
|
||||
this._tabList = document.getElementById("tab-list");
|
||||
},
|
||||
|
||||
/** Open a file as a preview tab (single-click).
|
||||
* Replaces any existing preview tab. If the file is already
|
||||
* open as a persistent tab, just activates it. */
|
||||
async openPreview(vault, path) {
|
||||
// Delegate to active pane's TabManager when split
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().openPreview(vault, path);
|
||||
}
|
||||
const tabId = `${vault}::${path}`;
|
||||
|
||||
// If already open as persistent tab, just activate it
|
||||
@@ -1977,6 +1926,10 @@ export const TabManager = {
|
||||
/** Convert a preview tab to a persistent tab (double-click).
|
||||
* If already persistent, opens a new duplicate (same file, different tab). */
|
||||
async openPersistent(vault, path) {
|
||||
// Delegate to active pane's TabManager when split
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().openPersistent(vault, path);
|
||||
}
|
||||
const tabId = `${vault}::${path}`;
|
||||
|
||||
// If it's already a preview tab, convert it to persistent
|
||||
@@ -2041,7 +1994,7 @@ export const TabManager = {
|
||||
|
||||
this._activeTabId = tabId;
|
||||
this._renderTabs();
|
||||
this._updatePaneActiveTab();
|
||||
|
||||
// Load tab content
|
||||
const cache = this._tabCache[tabId];
|
||||
if (!cache) return;
|
||||
@@ -2051,7 +2004,7 @@ export const TabManager = {
|
||||
state.currentPath = cache.path;
|
||||
syncActiveFileTreeItem(cache.vault, cache.path);
|
||||
|
||||
const area = this._getContentArea();
|
||||
const area = document.getElementById("content-area");
|
||||
|
||||
if (cache.data) {
|
||||
// Use cached data
|
||||
@@ -2089,6 +2042,10 @@ export const TabManager = {
|
||||
|
||||
/** Close a tab */
|
||||
close(tabId) {
|
||||
// Delegate to active pane's TabManager when split
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().close(tabId);
|
||||
}
|
||||
const idx = this._tabs.findIndex(t => t.id === tabId);
|
||||
if (idx === -1) return;
|
||||
|
||||
@@ -2111,6 +2068,9 @@ export const TabManager = {
|
||||
|
||||
/** Close all tabs */
|
||||
closeAll() {
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().closeAll();
|
||||
}
|
||||
this._tabs = [];
|
||||
this._tabCache = {};
|
||||
this._dirtyTabs.clear();
|
||||
@@ -2121,6 +2081,9 @@ export const TabManager = {
|
||||
|
||||
/** Close tabs to the right */
|
||||
closeRight(tabId) {
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().closeRight(tabId);
|
||||
}
|
||||
const idx = this._tabs.findIndex(t => t.id === tabId);
|
||||
if (idx === -1) return;
|
||||
const toClose = this._tabs.slice(idx + 1);
|
||||
@@ -2137,6 +2100,9 @@ export const TabManager = {
|
||||
|
||||
/** Close other tabs */
|
||||
closeOthers(tabId) {
|
||||
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.getActiveTabManager().closeOthers(tabId);
|
||||
}
|
||||
const tab = this._tabs.find(t => t.id === tabId);
|
||||
if (!tab) return;
|
||||
for (const t of this._tabs) {
|
||||
@@ -2163,7 +2129,7 @@ export const TabManager = {
|
||||
const cache = this._tabCache[this._activeTabId];
|
||||
if (!cache) return;
|
||||
|
||||
const area = this._getContentArea();
|
||||
const area = document.getElementById("content-area");
|
||||
const rendered = document.getElementById("file-rendered-content");
|
||||
|
||||
cache.scrollTop = area.scrollTop;
|
||||
@@ -2208,7 +2174,7 @@ export const TabManager = {
|
||||
},
|
||||
|
||||
_showDashboard() {
|
||||
const area = this._getContentArea();
|
||||
const area = document.getElementById("content-area");
|
||||
// Save dashboard DOM before clearing (it may have been removed from DOM by renderFile)
|
||||
let dashboard = document.getElementById("dashboard-home");
|
||||
if (!dashboard) {
|
||||
|
||||
@@ -12,10 +12,12 @@ import { openShareDialog } from './config.js';
|
||||
import { cacheViewedFile, getCachedFile } from './offline.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
// Pane-aware content area helper
|
||||
// Pane-aware content area helper — delegates to PaneManager when split
|
||||
function getContentArea() {
|
||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
||||
return window.PaneManager.getActiveContentArea();
|
||||
// Override set by PaneTabManager._renderFileInPane for cross-module rendering
|
||||
if (window._activePaneContentArea) return window._activePaneContentArea;
|
||||
if (window.PaneManager && window.PaneManager.isSplit() && window.getActiveContentArea) {
|
||||
return window.getActiveContentArea();
|
||||
}
|
||||
return document.getElementById('content-area');
|
||||
}
|
||||
|
||||
@@ -76,14 +76,16 @@ class TestPaneManagerStaticAssets:
|
||||
# Only the fallback inside getContentArea should remain
|
||||
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
|
||||
|
||||
def test_ui_tab_manager_has_pane_helpers(self):
|
||||
"""TabManager in ui.js has _getContentArea, _getTabBar, _getTabList."""
|
||||
def test_ui_tab_manager_has_delegation(self):
|
||||
"""TabManager in ui.js delegates to getActiveTabManager() when split."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
assert "_getContentArea()" in content
|
||||
assert "_getTabBar()" in content
|
||||
assert "_getTabList()" in content
|
||||
assert "activatePaneTab(" in content
|
||||
assert "getActiveTabManager" in content
|
||||
assert "PaneManager.isSplit()" in content
|
||||
# Must have delegation in entry-point methods
|
||||
assert "window.getActiveTabManager().openPreview" in content
|
||||
assert "window.getActiveTabManager().openPersistent" in content
|
||||
assert "window.getActiveTabManager().close(" in content
|
||||
|
||||
def test_ui_context_menu_has_split_actions(self):
|
||||
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""
|
||||
|
||||
Reference in New Issue
Block a user