feat: add CodeMirror dependencies and implement markdown editor UI
This commit is contained in:
@@ -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'];
|
||||
|
||||
Reference in New Issue
Block a user