feat(graph): Phase 3 — type filter, export PNG, fullscreen, focus node
Some checks failed
CI / lint (push) Has started running
CI / test (push) Has been cancelled
CI / security (push) Has been cancelled
CI / build (push) Has been cancelled

- Type filter checkboxes (dossier, .md, autre) in legend
- Export PNG button (canvas.toDataURL)
- Fullscreen button (Fullscreen API)
- Focus node function (center on specific node)
- Filter applied during _draw() to skip hidden nodes
This commit is contained in:
Bruno Charest 2026-05-28 14:48:31 -04:00
parent bf836caccc
commit 0416266dde
2 changed files with 68 additions and 4 deletions

View File

@ -717,6 +717,12 @@
<button class="editor-btn" id="graph-reset" title="Réinitialiser la vue" aria-label="Réinitialiser">
<i data-lucide="maximize" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-export" title="Exporter en PNG" aria-label="Export PNG">
<i data-lucide="download" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-fullscreen" title="Plein écran" aria-label="Plein écran">
<i data-lucide="expand" style="width:16px;height:16px"></i>
</button>
<button class="editor-btn" id="graph-close" title="Fermer" aria-label="Fermer">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
@ -725,10 +731,11 @@
<div class="editor-body" style="flex:1;overflow:hidden;position:relative;padding:0">
<canvas id="graph-canvas" style="width:100%;height:100%;cursor:grab"></canvas>
<div id="graph-tooltip" style="display:none;position:absolute;pointer-events:none;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:6px 10px;font-size:0.75rem;color:var(--text-primary);max-width:250px;z-index:10;box-shadow:0 2px 8px rgba(0,0,0,0.3)"></div>
<div id="graph-legend" style="position:absolute;bottom:12px;left:12px;display:flex;gap:16px;font-size:0.7rem;color:var(--text-muted);background:var(--bg-primary);padding:6px 12px;border-radius:6px;border:1px solid var(--border-color)">
<span>🟦 Dossier</span>
<span>🟩 Fichier .md</span>
<span>⬜ Autre fichier</span>
<div id="graph-legend" style="position:absolute;bottom:12px;left:12px;display:flex;gap:12px;font-size:0.7rem;color:var(--text-muted);background:var(--bg-primary);padding:6px 12px;border-radius:6px;border:1px solid var(--border-color);align-items:center;flex-wrap:wrap">
<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="graph-filter-dir" checked> 🟦 Dossier</label>
<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="graph-filter-md" checked> 🟩 .md</label>
<label style="cursor:pointer;display:flex;align-items:center;gap:3px"><input type="checkbox" id="graph-filter-other" checked> ⬜ Autre</label>
<span style="color:var(--text-muted);margin:0 4px"></span>
<span style="color:var(--text-muted)">── Parent</span>
<span style="color:var(--accent-color,#2563eb)">┅┅ Wikilink</span>
<span style="color:#e74c3c">← Backlink</span>

View File

@ -300,6 +300,7 @@ export const GraphViewManager = {
// Draw nodes
const searchLower = this._searchTerm.toLowerCase();
for (const node of this._nodes) {
if (!this._isNodeVisible(node)) continue;
const p = this._nodePositions[node.id];
if (!p) continue;
@ -395,6 +396,48 @@ export const GraphViewManager = {
if (this._tooltipEl) this._tooltipEl.style.display = 'none';
},
// --- Advanced controls ---
_isNodeVisible(node) {
const showDir = document.getElementById('graph-filter-dir')?.checked ?? true;
const showMd = document.getElementById('graph-filter-md')?.checked ?? true;
const showOther = document.getElementById('graph-filter-other')?.checked ?? true;
if (node.type === 'directory') return showDir;
if (node.type === 'file' && (node.path || '').endsWith('.md')) return showMd;
if (node.type === 'file') return showOther;
return true;
},
applyTypeFilter() {
// Filters are applied during _draw() — nodes/edges not in visible set are skipped
},
exportPNG() {
const link = document.createElement('a');
link.download = `obsigate-graph-${this._vault}-${Date.now()}.png`;
link.href = this._canvas.toDataURL('image/png');
link.click();
},
toggleFullscreen() {
const container = this._canvas?.parentElement?.parentElement; // editor-container
if (!container) return;
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
container.requestFullscreen();
}
// Re-init layout after fullscreen change
setTimeout(() => this._onResize(), 300);
},
focusNode(nodeId) {
const pos = this._nodePositions[nodeId];
if (!pos) return;
this._offsetX = this._width / 2 - pos.x * this._zoom;
this._offsetY = this._height / 2 - pos.y * this._zoom;
},
_onMouseDown(e) {
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
@ -520,6 +563,20 @@ export function initGraphView() {
});
}
// Export PNG
const exportBtn = document.getElementById('graph-export');
if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());
// Fullscreen
const fullscreenBtn = document.getElementById('graph-fullscreen');
if (fullscreenBtn) fullscreenBtn.addEventListener('click', () => gm.toggleFullscreen());
// Type filters
['dir', 'md', 'other'].forEach(type => {
const cb = document.getElementById(`graph-filter-${type}`);
if (cb) cb.addEventListener('change', () => gm.applyTypeFilter());
});
if (canvas) {
canvas.addEventListener('mousedown', (e) => gm._onMouseDown(e));
canvas.addEventListener('mousemove', (e) => gm._onMouseMove(e));