import { Component, inject, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TocService, TocItem } from '../../services/toc.service';
@Component({
selector: 'app-toc-panel',
standalone: true,
imports: [CommonModule],
template: `
@if (visibleTocItems().length === 0) {
Aucun titre trouvé
} @else {
@for (item of visibleTocItems(); track item.id) {
}
}
`,
styles: [`
.toc-panel {
background: var(--toc-bg, #111827);
color: var(--toc-fg, #e5e7eb);
border-left: 1px solid var(--toc-border, rgba(148, 163, 184, 0.18));
}
.toc-header {
border-bottom: 1px solid var(--toc-border, rgba(148, 163, 184, 0.18));
color: inherit;
}
.toc-close-btn {
color: inherit;
}
.toc-close-btn:hover {
background: color-mix(in srgb, rgba(148, 163, 184, 0.15) 60%, transparent);
}
.toc-empty {
color: var(--toc-muted, rgba(148, 163, 184, 0.75));
}
.toc-footer {
border-top: 1px solid var(--toc-border, rgba(148, 163, 184, 0.18));
color: var(--toc-muted, rgba(148, 163, 184, 0.75));
}
.toc-item {
border: 1px solid transparent;
background: color-mix(in srgb, var(--toc-bg, #111827) 70%, rgba(148, 163, 184, 0.12) 30%);
color: inherit;
box-shadow: inset 0 0 0 0 transparent;
}
.toc-item:hover {
border-color: color-mix(in srgb, var(--toc-active, #6366f1) 35%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--toc-active, #6366f1) 25%, transparent);
background: color-mix(in srgb, rgba(99, 102, 241, 0.18) 40%, var(--toc-bg, #111827));
}
.toc-item:focus-visible {
outline: 2px solid var(--toc-active, #6366f1);
outline-offset: 2px;
}
.toc-item-h1 { padding-left: 0.25rem; font-weight: 600; }
.toc-item-h2 { padding-left: 1.25rem; font-weight: 500; }
.toc-item-h3 { padding-left: 2.25rem; font-weight: 500; font-size: 0.8125rem; color: var(--toc-muted, rgba(148, 163, 184, 0.75)); }
.toc-item-active {
border-color: color-mix(in srgb, var(--toc-active, #6366f1) 60%, transparent);
background: color-mix(in srgb, var(--toc-active, #6366f1) 18%, transparent);
color: var(--toc-active, #6366f1);
}
.toc-text {
display: flex;
justify-content: space-between;
gap: 0.5rem;
color: inherit;
}
.toc-level {
font-size: 0.75rem;
color: var(--toc-muted, rgba(148, 163, 184, 0.75));
}
`]
})
export class TocPanelComponent {
readonly tocService = inject(TocService);
@Input() mode: 'fixed' | 'container' = 'fixed';
private collapsed = new Set();
getTocItemClass(item: TocItem): string {
switch (item.level) {
case 1: return 'toc-item-h1';
case 2: return 'toc-item-h2';
case 3: return 'toc-item-h3';
default: return 'toc-item-h3';
}
}
get panelClass(): string {
const base = 'toc-panel shadow-xl z-40';
if (this.mode === 'container') {
return `${base} h-full overflow-hidden`;
}
return `${base} fixed right-0 top-0 bottom-0 overflow-y-auto`;
}
onItemClick(item: TocItem, ev?: MouseEvent): void {
// Shift+Click on H1/H2 toggles collapse/expand
if ((ev?.shiftKey) && this.isCollapsible(item)) {
this.toggleCollapse(item);
return;
}
this.ensureExpandedFor(item);
this.tocService.scrollToHeading(item.blockId);
}
ngOnChanges(): void { this.maybeFocusFirst(); }
ngAfterViewChecked(): void { this.maybeFocusFirst(); }
private lastFocused = false;
private maybeFocusFirst() {
// When panel opens, focus first item once
if (this.tocService.isOpen() && !this.lastFocused) {
const root = (document.getElementById('toc-panel')) as HTMLElement | null;
const btn = root?.querySelector('button');
(btn as HTMLElement | null)?.focus?.();
this.lastFocused = true;
}
if (!this.tocService.isOpen() && this.lastFocused) this.lastFocused = false;
// Auto-expand ancestors for active item
this.ensureExpandedForActive();
}
onKeydown(ev: KeyboardEvent) {
const root = document.getElementById('toc-panel') as HTMLElement | null;
if (!root) return;
const items = Array.from(root.querySelectorAll('button')) as HTMLElement[];
if (!items.length) return;
const active = document.activeElement as HTMLElement | null;
let idx = Math.max(0, items.findIndex(b => b === active));
const move = (delta: number) => {
idx = (idx + delta + items.length) % items.length;
items[idx]?.focus?.();
};
switch (ev.key) {
case 'ArrowDown': move(1); ev.preventDefault(); break;
case 'ArrowUp': move(-1); ev.preventDefault(); break;
case 'Home': idx = 0; items[idx]?.focus?.(); ev.preventDefault(); break;
case 'End': idx = items.length - 1; items[idx]?.focus?.(); ev.preventDefault(); break;
case 'Enter':
case ' ': (active as HTMLButtonElement | null)?.click?.(); ev.preventDefault(); break;
case 'Tab': {
// Focus trap inside panel
const shift = ev.shiftKey;
if (shift && idx === 0) { items[items.length - 1]?.focus?.(); ev.preventDefault(); }
else if (!shift && idx === items.length - 1) { items[0]?.focus?.(); ev.preventDefault(); }
break;
}
}
}
isCollapsible(item: TocItem): boolean { return item.level === 1 || item.level === 2; }
isCollapsed(item: TocItem): boolean { return this.collapsed.has(item.blockId); }
toggleCollapse(item: TocItem): void {
if (!this.isCollapsible(item)) return;
if (this.isCollapsed(item)) this.collapsed.delete(item.blockId); else this.collapsed.add(item.blockId);
}
visibleTocItems(): TocItem[] {
const items = this.tocService.tocItems();
const out: TocItem[] = [];
let hideLevel1: string | null = null;
let hideLevel2: string | null = null;
for (let i = 0; i < items.length; i++) {
const it = items[i];
if (it.level === 1) {
hideLevel2 = null;
hideLevel1 = this.isCollapsed(it) ? it.blockId : null;
out.push(it);
continue;
}
if (it.level === 2) {
if (hideLevel1) continue; // hidden under collapsed H1
hideLevel2 = this.isCollapsed(it) ? it.blockId : null;
out.push(it);
continue;
}
// level 3
if (hideLevel1 || hideLevel2) continue;
out.push(it);
}
return out;
}
private ensureExpandedForActive() {
const active = this.tocService.activeId();
if (!active) return;
const items = this.tocService.tocItems();
const idx = items.findIndex(it => it.blockId === active);
if (idx < 0) return;
// Expand nearest ancestors (H2 then H1 above)
for (let i = idx - 1; i >= 0; i--) {
const it = items[i];
if (it.level === 3) continue;
if (it.level === 2) { this.collapsed.delete(it.blockId); }
if (it.level === 1) { this.collapsed.delete(it.blockId); break; }
}
}
private ensureExpandedFor(item: TocItem) {
// When navigating to item, expand its ancestors
if (item.level === 3) {
const items = this.tocService.tocItems();
const idx = items.findIndex(it => it.blockId === item.blockId);
for (let i = idx - 1; i >= 0; i--) {
const it = items[i];
if (it.level === 2) this.collapsed.delete(it.blockId);
if (it.level === 1) { this.collapsed.delete(it.blockId); break; }
}
} else if (item.level === 2) {
const items = this.tocService.tocItems();
const idx = items.findIndex(it => it.blockId === item.blockId);
for (let i = idx - 1; i >= 0; i--) { const it = items[i]; if (it.level === 1) { this.collapsed.delete(it.blockId); break; } }
}
}
}