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,
|
_tabList: null,
|
||||||
_dirtyTabs: new Set(),
|
_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() {
|
init() {
|
||||||
this._tabBar = this._getTabBar();
|
this._tabBar = document.getElementById("tab-bar");
|
||||||
this._tabList = this._getTabList();
|
this._tabList = document.getElementById("tab-list");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Open a file as a preview tab (single-click).
|
/** Open a file as a preview tab (single-click).
|
||||||
* Replaces any existing preview tab. If the file is already
|
* Replaces any existing preview tab. If the file is already
|
||||||
* open as a persistent tab, just activates it. */
|
* open as a persistent tab, just activates it. */
|
||||||
async openPreview(vault, path) {
|
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}`;
|
const tabId = `${vault}::${path}`;
|
||||||
|
|
||||||
// If already open as persistent tab, just activate it
|
// 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).
|
/** Convert a preview tab to a persistent tab (double-click).
|
||||||
* If already persistent, opens a new duplicate (same file, different tab). */
|
* If already persistent, opens a new duplicate (same file, different tab). */
|
||||||
async openPersistent(vault, path) {
|
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}`;
|
const tabId = `${vault}::${path}`;
|
||||||
|
|
||||||
// If it's already a preview tab, convert it to persistent
|
// If it's already a preview tab, convert it to persistent
|
||||||
@@ -2041,7 +1994,7 @@ export const TabManager = {
|
|||||||
|
|
||||||
this._activeTabId = tabId;
|
this._activeTabId = tabId;
|
||||||
this._renderTabs();
|
this._renderTabs();
|
||||||
this._updatePaneActiveTab();
|
|
||||||
// Load tab content
|
// Load tab content
|
||||||
const cache = this._tabCache[tabId];
|
const cache = this._tabCache[tabId];
|
||||||
if (!cache) return;
|
if (!cache) return;
|
||||||
@@ -2051,7 +2004,7 @@ export const TabManager = {
|
|||||||
state.currentPath = cache.path;
|
state.currentPath = cache.path;
|
||||||
syncActiveFileTreeItem(cache.vault, cache.path);
|
syncActiveFileTreeItem(cache.vault, cache.path);
|
||||||
|
|
||||||
const area = this._getContentArea();
|
const area = document.getElementById("content-area");
|
||||||
|
|
||||||
if (cache.data) {
|
if (cache.data) {
|
||||||
// Use cached data
|
// Use cached data
|
||||||
@@ -2089,6 +2042,10 @@ export const TabManager = {
|
|||||||
|
|
||||||
/** Close a tab */
|
/** Close a tab */
|
||||||
close(tabId) {
|
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);
|
const idx = this._tabs.findIndex(t => t.id === tabId);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
||||||
@@ -2111,6 +2068,9 @@ export const TabManager = {
|
|||||||
|
|
||||||
/** Close all tabs */
|
/** Close all tabs */
|
||||||
closeAll() {
|
closeAll() {
|
||||||
|
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||||
|
return window.getActiveTabManager().closeAll();
|
||||||
|
}
|
||||||
this._tabs = [];
|
this._tabs = [];
|
||||||
this._tabCache = {};
|
this._tabCache = {};
|
||||||
this._dirtyTabs.clear();
|
this._dirtyTabs.clear();
|
||||||
@@ -2121,6 +2081,9 @@ export const TabManager = {
|
|||||||
|
|
||||||
/** Close tabs to the right */
|
/** Close tabs to the right */
|
||||||
closeRight(tabId) {
|
closeRight(tabId) {
|
||||||
|
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||||
|
return window.getActiveTabManager().closeRight(tabId);
|
||||||
|
}
|
||||||
const idx = this._tabs.findIndex(t => t.id === tabId);
|
const idx = this._tabs.findIndex(t => t.id === tabId);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
const toClose = this._tabs.slice(idx + 1);
|
const toClose = this._tabs.slice(idx + 1);
|
||||||
@@ -2137,6 +2100,9 @@ export const TabManager = {
|
|||||||
|
|
||||||
/** Close other tabs */
|
/** Close other tabs */
|
||||||
closeOthers(tabId) {
|
closeOthers(tabId) {
|
||||||
|
if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
|
||||||
|
return window.getActiveTabManager().closeOthers(tabId);
|
||||||
|
}
|
||||||
const tab = this._tabs.find(t => t.id === tabId);
|
const tab = this._tabs.find(t => t.id === tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
for (const t of this._tabs) {
|
for (const t of this._tabs) {
|
||||||
@@ -2163,7 +2129,7 @@ export const TabManager = {
|
|||||||
const cache = this._tabCache[this._activeTabId];
|
const cache = this._tabCache[this._activeTabId];
|
||||||
if (!cache) return;
|
if (!cache) return;
|
||||||
|
|
||||||
const area = this._getContentArea();
|
const area = document.getElementById("content-area");
|
||||||
const rendered = document.getElementById("file-rendered-content");
|
const rendered = document.getElementById("file-rendered-content");
|
||||||
|
|
||||||
cache.scrollTop = area.scrollTop;
|
cache.scrollTop = area.scrollTop;
|
||||||
@@ -2208,7 +2174,7 @@ export const TabManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_showDashboard() {
|
_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)
|
// Save dashboard DOM before clearing (it may have been removed from DOM by renderFile)
|
||||||
let dashboard = document.getElementById("dashboard-home");
|
let dashboard = document.getElementById("dashboard-home");
|
||||||
if (!dashboard) {
|
if (!dashboard) {
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import { openShareDialog } from './config.js';
|
|||||||
import { cacheViewedFile, getCachedFile } from './offline.js';
|
import { cacheViewedFile, getCachedFile } from './offline.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
// Pane-aware content area helper
|
// Pane-aware content area helper — delegates to PaneManager when split
|
||||||
function getContentArea() {
|
function getContentArea() {
|
||||||
if (window.PaneManager && window.PaneManager.isSplit()) {
|
// Override set by PaneTabManager._renderFileInPane for cross-module rendering
|
||||||
return window.PaneManager.getActiveContentArea();
|
if (window._activePaneContentArea) return window._activePaneContentArea;
|
||||||
|
if (window.PaneManager && window.PaneManager.isSplit() && window.getActiveContentArea) {
|
||||||
|
return window.getActiveContentArea();
|
||||||
}
|
}
|
||||||
return document.getElementById('content-area');
|
return document.getElementById('content-area');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,14 +76,16 @@ class TestPaneManagerStaticAssets:
|
|||||||
# Only the fallback inside getContentArea should remain
|
# Only the fallback inside getContentArea should remain
|
||||||
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
|
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
|
||||||
|
|
||||||
def test_ui_tab_manager_has_pane_helpers(self):
|
def test_ui_tab_manager_has_delegation(self):
|
||||||
"""TabManager in ui.js has _getContentArea, _getTabBar, _getTabList."""
|
"""TabManager in ui.js delegates to getActiveTabManager() when split."""
|
||||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||||
content = path.read_text()
|
content = path.read_text()
|
||||||
assert "_getContentArea()" in content
|
assert "getActiveTabManager" in content
|
||||||
assert "_getTabBar()" in content
|
assert "PaneManager.isSplit()" in content
|
||||||
assert "_getTabList()" in content
|
# Must have delegation in entry-point methods
|
||||||
assert "activatePaneTab(" in content
|
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):
|
def test_ui_context_menu_has_split_actions(self):
|
||||||
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""
|
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""
|
||||||
|
|||||||
Reference in New Issue
Block a user