feat: add CodeMirror dependencies and implement markdown editor UI

This commit is contained in:
2025-10-20 15:43:56 -04:00
parent 4d995bf7a9
commit 062d743481
17 changed files with 2541 additions and 13 deletions
@@ -1,9 +1,10 @@
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
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
@@ -45,6 +46,19 @@ import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-ed
</span>
</div>
<div class="markdown-viewer__toolbar-right">
<!-- Edit Button -->
<button
*ngIf="!isExcalidrawFile()"
type="button"
class="btn-standard-icon"
(click)="toggleEditMode()"
[attr.aria-label]="'Edit markdown'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<!-- Fullscreen Button -->
<button
*ngIf="fullscreenMode"
type="button"
@@ -176,6 +190,7 @@ import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-ed
export class MarkdownViewerComponent implements OnChanges {
private markdownService = inject(MarkdownService);
private sanitizer = inject(DomSanitizer);
private editorState = inject(EditorStateService);
/** Contenu markdown brut à afficher */
@Input() content: string = '';
@@ -194,6 +209,9 @@ export class MarkdownViewerComponent implements OnChanges {
/** 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<boolean>(false);
@@ -246,6 +264,19 @@ export class MarkdownViewerComponent implements OnChanges {
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(() => {
@@ -1,8 +1,9 @@
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, CUSTOM_ELEMENTS_SCHEMA, ViewContainerRef, ComponentRef, ViewChild, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MarkdownViewerComponent } from '../markdown-viewer/markdown-viewer.component';
import { FileTypeDetectorService } from '../../services/file-type-detector.service';
import { Note } from '../../types';
import { EditorStateService } from '../../services/editor-state.service';
/**
* Composant intelligent qui détecte automatiquement le type de fichier
@@ -25,15 +26,19 @@ import { Note } from '../../types';
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<div class="smart-file-viewer" [attr.data-viewer-type]="viewerType()">
<!-- Markdown/Excalidraw Viewer -->
<!-- Markdown Editor (Edit Mode) -->
<div #editorContainer *ngIf="isEditMode()" class="smart-file-viewer__editor"></div>
<!-- Markdown/Excalidraw Viewer (Read Mode) -->
<app-markdown-viewer
*ngIf="viewerType() === 'markdown' || viewerType() === 'excalidraw'"
*ngIf="!isEditMode() && (viewerType() === 'markdown' || viewerType() === 'excalidraw')"
[content]="content"
[allNotes]="allNotes"
[currentNote]="currentNote"
[showToolbar]="showToolbar"
[fullscreenMode]="fullscreenMode"
[filePath]="filePath">
[filePath]="filePath"
(editModeRequested)="onEditModeRequested($event)">
</app-markdown-viewer>
<!-- Image Viewer -->
@@ -83,6 +88,11 @@ import { Note } from '../../types';
flex-direction: column;
}
.smart-file-viewer__editor {
flex: 1;
overflow: hidden;
}
.smart-file-viewer__image,
.smart-file-viewer__pdf,
.smart-file-viewer__text,
@@ -124,7 +134,11 @@ import { Note } from '../../types';
`]
})
export class SmartFileViewerComponent implements OnChanges {
@ViewChild('editorContainer', { read: ViewContainerRef }) editorContainer?: ViewContainerRef;
private fileTypeDetector = inject(FileTypeDetectorService);
private editorStateService = inject(EditorStateService);
private editorComponentRef?: ComponentRef<any>;
@Input() filePath: string = '';
@Input() content: string = '';
@@ -134,6 +148,7 @@ export class SmartFileViewerComponent implements OnChanges {
@Input() fullscreenMode: boolean = true;
viewerType = signal<'markdown' | 'excalidraw' | 'image' | 'pdf' | 'text' | 'unknown'>('unknown');
isEditMode = computed(() => this.editorStateService.isEditMode());
fileName = computed(() => {
return this.filePath.split('/').pop() || this.filePath.split('\\').pop() || 'Unknown file';
@@ -166,4 +181,64 @@ export class SmartFileViewerComponent implements OnChanges {
const type = this.fileTypeDetector.getViewerType(this.filePath, this.content);
this.viewerType.set(type);
}
constructor() {
// Observer le changement de mode édition
effect(() => {
const editMode = this.editorStateService.isEditMode();
if (editMode && !this.editorComponentRef) {
this.loadEditor();
} else if (!editMode && this.editorComponentRef) {
this.unloadEditor();
}
});
}
async onEditModeRequested(event: { path: string; content: string }): Promise<void> {
this.editorStateService.enterEditMode(event.path, event.content);
}
private async loadEditor(): Promise<void> {
if (!this.editorContainer) {
console.warn('[SmartFileViewer] Editor container not available');
return;
}
try {
// Lazy load the editor module
const { MarkdownEditorComponent } = await import('../../app/features/editor/markdown-editor.component');
// Clear any existing component
this.editorContainer.clear();
// Create the editor component
this.editorComponentRef = this.editorContainer.createComponent(MarkdownEditorComponent);
// Set inputs
const path = this.editorStateService.currentPath();
const content = this.editorStateService.content();
if (path && content !== null) {
this.editorComponentRef.setInput('initialPath', path);
this.editorComponentRef.setInput('initialContent', content);
}
console.log('[SmartFileViewer] Editor loaded successfully');
} catch (error) {
console.error('[SmartFileViewer] Failed to load editor:', error);
}
}
private unloadEditor(): void {
if (this.editorComponentRef) {
this.editorComponentRef.destroy();
this.editorComponentRef = undefined;
}
if (this.editorContainer) {
this.editorContainer.clear();
}
console.log('[SmartFileViewer] Editor unloaded');
}
}
@@ -16,6 +16,8 @@ import { Note } from '../../../types';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { NotePreviewService, PreviewData } from '../../../services/note-preview.service';
import { NoteHeaderComponent } from '../../../app/features/note/components/note-header/note-header.component';
import { MarkdownEditorComponent } from '../../../app/features/editor/markdown-editor.component';
import { EditorStateService } from '../../../services/editor-state.service';
import { ClipboardService } from '../../../app/shared/services/clipboard.service';
import { ToastService } from '../../../app/shared/toast/toast.service';
import { VaultService } from '../../../services/vault.service';
@@ -65,7 +67,7 @@ interface MetadataEntry {
@Component({
selector: 'app-note-viewer',
standalone: true,
imports: [CommonModule, NoteHeaderComponent],
imports: [CommonModule, NoteHeaderComponent, MarkdownEditorComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="relative p-1 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
@@ -82,6 +84,18 @@ interface MetadataEntry {
></app-note-header>
<div class="flex items-center gap-1">
<button
type="button"
class="note-toolbar-icon"
(click)="enterEdit()"
title="Éditer"
aria-label="Éditer"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button
type="button"
class="note-toolbar-icon"
@@ -132,6 +146,13 @@ interface MetadataEntry {
</div>
</div>
@if (isEditMode()) {
<app-markdown-editor
[initialPath]="note().filePath"
[initialContent]="note().rawContent ?? note().content"
/>
} @else {
@if (frontmatterTags().length > 0) {
<div class="mb-6 md-tag-group not-prose">
@for (tag of frontmatterTags(); track tag) {
@@ -320,6 +341,8 @@ interface MetadataEntry {
</ul>
</div>
}
}
</div>
`,
})
@@ -344,6 +367,7 @@ export class NoteViewerComponent implements OnDestroy {
private readonly clipboard = inject(ClipboardService);
private readonly toast = inject(ToastService);
private readonly vault = inject(VaultService);
private readonly editorState = inject(EditorStateService);
private readonly tagPaletteSize = 12;
private readonly tagColorCache = new Map<string, number>();
private readonly copyFeedbackTimers = new Map<HTMLElement, number>();
@@ -365,6 +389,9 @@ export class NoteViewerComponent implements OnDestroy {
readonly maxMetadataPreviewItems = 3;
readonly copyStatus = signal('');
// Edition state
readonly isEditMode = this.editorState.isEditMode;
readonly sanitizedHtmlContent = computed<SafeHtml>(() =>
this.sanitizer.bypassSecurityTrustHtml(this.noteHtmlContent())
);
@@ -536,6 +563,13 @@ export class NoteViewerComponent implements OnDestroy {
this.menuOpen.set(false);
}
enterEdit(): void {
const n = this.note();
if (!n) return;
const content = n.rawContent ?? n.content ?? '';
this.editorState.enterEditMode(n.filePath, content);
}
getAuthorFromFrontmatter(): string | null {
const frontmatter = this.note().frontmatter ?? {};
const authorValue = frontmatter['author'] ?? frontmatter['auteur'];