import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { MarkdownService } from '../../services/markdown.service';
import { Note } from '../../types';
import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-editor.component';
import { EditorStateService } from '../../services/editor-state.service';
/**
* Composant réutilisable pour afficher du contenu Markdown
*
* Features:
* - Rendu markdown complet (GFM, callouts, math, mermaid, etc.)
* - Support des fichiers .excalidraw.md avec éditeur intégré
* - Lazy loading des images
* - Mode plein écran
* - Syntax highlighting avec highlight.js
* - Support des WikiLinks et tags inline
*
* @example
* ```html
*
*
* ```
*/
@Component({
selector: 'app-markdown-viewer',
standalone: true,
imports: [CommonModule, DrawingsEditorComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
`,
styles: [`
:host {
display: block;
height: 100%;
}
.markdown-viewer {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--bg-main);
}
.markdown-viewer__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background-color: var(--card);
}
.markdown-viewer__toolbar-left,
.markdown-viewer__toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.markdown-viewer__content {
flex: 1;
overflow-y: auto;
padding: 2rem;
}
.markdown-viewer__excalidraw {
flex: 1;
overflow: hidden;
}
.markdown-viewer__error,
.markdown-viewer__loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.markdown-viewer--fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: var(--bg-main);
}
/* Responsive padding */
@media (max-width: 768px) {
.markdown-viewer__content {
padding: 1rem;
}
}
/* Lazy loading images */
:host ::ng-deep .markdown-viewer__content img {
opacity: 0;
transition: opacity 0.3s ease;
}
:host ::ng-deep .markdown-viewer__content img.loaded {
opacity: 1;
}
/* Smooth scrolling */
.markdown-viewer__content {
scroll-behavior: smooth;
}
`]
})
export class MarkdownViewerComponent implements OnChanges {
private markdownService = inject(MarkdownService);
private sanitizer = inject(DomSanitizer);
private editorState = inject(EditorStateService);
/** Contenu markdown brut à afficher */
@Input() content: string = '';
/** Liste de toutes les notes pour la résolution des WikiLinks */
@Input() allNotes: Note[] = [];
/** Note courante pour la résolution des chemins relatifs */
@Input() currentNote?: Note;
/** Afficher la barre d'outils */
@Input() showToolbar: boolean = true;
/** Activer le mode plein écran */
@Input() fullscreenMode: boolean = false;
/** Chemin du fichier (pour détecter les .excalidraw.md) */
@Input() filePath: string = '';
/** Event émis quand on veut passer en mode édition */
@Output() editModeRequested = new EventEmitter<{ path: string; content: string }>();
// Signals
isLoading = signal(false);
error = signal(null);
isFullscreen = signal(false);
isExcalidrawFile = computed(() => {
return this.filePath.toLowerCase().endsWith('.excalidraw.md');
});
excalidrawPath = computed(() => {
return this.isExcalidrawFile() ? this.filePath : '';
});
renderedHtml = computed(() => {
if (!this.content || this.isExcalidrawFile()) {
return '';
}
try {
const html = this.markdownService.render(
this.content,
this.allNotes,
this.currentNote
);
return this.sanitizer.bypassSecurityTrustHtml(html);
} catch (err) {
console.error('Markdown render error:', err);
this.error.set(`Erreur de rendu: ${err}`);
return '';
}
});
constructor() {
// Setup lazy loading for images
effect(() => {
if (!this.isExcalidrawFile()) {
this.setupLazyLoading();
}
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['content'] || changes['filePath']) {
this.error.set(null);
}
}
toggleFullscreen(): void {
this.isFullscreen.update(v => !v);
}
toggleEditMode(): void {
if (!this.filePath) {
console.warn('[MarkdownViewer] Cannot edit: no file path');
return;
}
// Émettre l'événement avec le chemin et le contenu
this.editModeRequested.emit({
path: this.filePath,
content: this.content
});
}
private setupLazyLoading(): void {
// Wait for next tick to ensure DOM is updated
setTimeout(() => {
const images = document.querySelectorAll('.markdown-viewer__content img:not(.loaded)');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, {
rootMargin: '50px'
});
images.forEach(img => imageObserver.observe(img));
} else {
// Fallback: load all images immediately
images.forEach(img => img.classList.add('loaded'));
}
}, 0);
}
}