443 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			443 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Mode Édition Markdown - Documentation Technique
 | |
| 
 | |
| ## 🎯 Vue d'ensemble
 | |
| 
 | |
| Le mode édition Markdown permet aux utilisateurs d'éditer directement leurs fichiers Markdown dans ObsiViewer avec un éditeur professionnel basé sur **CodeMirror 6**. L'éditeur est chargé dynamiquement (lazy loading) uniquement lors du passage en mode édition pour préserver les performances.
 | |
| 
 | |
| ## 📁 Architecture
 | |
| 
 | |
| ### Structure des fichiers
 | |
| 
 | |
| ```
 | |
| src/
 | |
| ├── app/
 | |
| │   └── features/
 | |
| │       └── editor/
 | |
| │           ├── markdown-editor.module.ts           # Module lazy-loadable
 | |
| │           ├── markdown-editor.component.ts        # Composant principal de l'éditeur
 | |
| │           └── editor-can-deactivate.guard.ts     # Guard de navigation
 | |
| ├── services/
 | |
| │   └── editor-state.service.ts                    # Service de gestion d'état
 | |
| ├── components/
 | |
| │   ├── markdown-viewer/
 | |
| │   │   └── markdown-viewer.component.ts           # Bouton "Éditer" ajouté
 | |
| │   └── smart-file-viewer/
 | |
| │       └── smart-file-viewer.component.ts         # Intégration éditeur
 | |
| └── styles/
 | |
|     └── codemirror.css                             # Styles globaux CodeMirror
 | |
| ```
 | |
| 
 | |
| ### Flux de données
 | |
| 
 | |
| ```
 | |
| ┌─────────────────────┐
 | |
| │ markdown-viewer     │
 | |
| │ (Bouton "Éditer")   │
 | |
| └──────────┬──────────┘
 | |
|            │ editModeRequested(path, content)
 | |
|            ▼
 | |
| ┌─────────────────────┐
 | |
| │ smart-file-viewer   │
 | |
| │ onEditModeRequested │
 | |
| └──────────┬──────────┘
 | |
|            │ enterEditMode(path, content)
 | |
|            ▼
 | |
| ┌─────────────────────┐
 | |
| │ editor-state.service│
 | |
| │ (mode: 'edit')      │
 | |
| └──────────┬──────────┘
 | |
|            │ effect()
 | |
|            ▼
 | |
| ┌─────────────────────┐
 | |
| │ smart-file-viewer   │
 | |
| │ loadEditor()        │
 | |
| └──────────┬──────────┘
 | |
|            │ dynamic import
 | |
|            ▼
 | |
| ┌─────────────────────┐
 | |
| │ markdown-editor     │
 | |
| │ (CodeMirror 6)      │
 | |
| └─────────────────────┘
 | |
| ```
 | |
| 
 | |
| ## 🔧 Composants clés
 | |
| 
 | |
| ### 1. EditorStateService (`editor-state.service.ts`)
 | |
| 
 | |
| Service singleton qui gère l'état global du mode édition.
 | |
| 
 | |
| **État géré:**
 | |
| ```typescript
 | |
| interface EditorState {
 | |
|   mode: 'view' | 'edit';      // Mode actuel
 | |
|   currentPath: string | null;  // Chemin du fichier en cours d'édition
 | |
|   isDirty: boolean;            // Modifications non sauvegardées
 | |
|   content: string | null;      // Contenu en cours d'édition
 | |
| }
 | |
| ```
 | |
| 
 | |
| **Méthodes principales:**
 | |
| - `enterEditMode(path, content)` - Passe en mode édition
 | |
| - `exitEditMode()` - Quitte le mode édition
 | |
| - `setDirty(isDirty)` - Marque le contenu comme modifié
 | |
| - `updateContent(content)` - Met à jour le contenu
 | |
| - `markAsSaved()` - Réinitialise le flag dirty après sauvegarde
 | |
| - `canExit()` - Vérifie si on peut quitter sans perdre de données
 | |
| 
 | |
| ### 2. MarkdownEditorComponent (`markdown-editor.component.ts`)
 | |
| 
 | |
| Composant standalone qui encapsule CodeMirror 6.
 | |
| 
 | |
| **Features:**
 | |
| - ✅ Édition Markdown avec coloration syntaxique
 | |
| - ✅ Support YAML front-matter
 | |
| - ✅ Line numbers, active line highlighting
 | |
| - ✅ Bracket matching, auto-closing
 | |
| - ✅ Search & replace (Ctrl/Cmd+F)
 | |
| - ✅ History (undo/redo)
 | |
| - ✅ Word wrap toggle
 | |
| - ✅ Autosave après 5s d'inactivité
 | |
| - ✅ Thème dark/light synchronisé
 | |
| - ✅ Responsive mobile
 | |
| 
 | |
| **Inputs:**
 | |
| - `initialPath: string` - Chemin du fichier
 | |
| - `initialContent: string` - Contenu initial
 | |
| 
 | |
| **Toolbar:**
 | |
| - **Save** (Ctrl/Cmd+S) - Sauvegarde via VaultService
 | |
| - **Wrap** - Toggle word wrap
 | |
| - **Undo** - Annuler (Ctrl/Cmd+Z)
 | |
| - **Redo** - Refaire (Ctrl/Cmd+Y)
 | |
| - **Close** - Quitter l'éditeur (avec confirmation si dirty)
 | |
| 
 | |
| ### 3. SmartFileViewerComponent (Intégration)
 | |
| 
 | |
| Gère le basculement entre mode lecture et mode édition.
 | |
| 
 | |
| **Lazy Loading:**
 | |
| ```typescript
 | |
| private async loadEditor(): Promise<void> {
 | |
|   const { MarkdownEditorComponent } = await import(
 | |
|     '../../app/features/editor/markdown-editor.component'
 | |
|   );
 | |
|   this.editorComponentRef = this.editorContainer.createComponent(
 | |
|     MarkdownEditorComponent
 | |
|   );
 | |
|   // Set inputs...
 | |
| }
 | |
| ```
 | |
| 
 | |
| **Avantages:**
 | |
| - CodeMirror 6 n'est chargé que lors du passage en mode édition
 | |
| - Pas d'impact sur les performances en mode lecture
 | |
| - Destruction automatique du composant lors de la sortie d'édition
 | |
| 
 | |
| ### 4. EditorCanDeactivateGuard (`editor-can-deactivate.guard.ts`)
 | |
| 
 | |
| Guard de navigation Angular pour empêcher la perte de données.
 | |
| 
 | |
| **Protection:**
 | |
| - Navigation Angular Router
 | |
| - Fermeture/rafraîchissement du navigateur (beforeunload)
 | |
| 
 | |
| **Usage dans les routes:**
 | |
| ```typescript
 | |
| {
 | |
|   path: 'note/:id',
 | |
|   component: NoteViewerComponent,
 | |
|   canDeactivate: [EditorCanDeactivateGuard]
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🎨 Styles et thème
 | |
| 
 | |
| ### Variables CSS (`codemirror.css`)
 | |
| 
 | |
| Les thèmes sont définis via des variables CSS qui s'adaptent automatiquement au mode dark/light du site:
 | |
| 
 | |
| ```css
 | |
| :root {
 | |
|   --cm-bg: #ffffff;
 | |
|   --cm-text: #1e293b;
 | |
|   --cm-cursor: #3b82f6;
 | |
|   --cm-selection-bg: #bfdbfe;
 | |
|   --cm-active-line-bg: #f1f5f9;
 | |
|   /* ... */
 | |
| }
 | |
| 
 | |
| .dark {
 | |
|   --cm-bg: #1e293b;
 | |
|   --cm-text: #e2e8f0;
 | |
|   --cm-cursor: #60a5fa;
 | |
|   /* ... */
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Détection du thème
 | |
| 
 | |
| Le composant observe les changements de classe `dark` sur `<html>`:
 | |
| 
 | |
| ```typescript
 | |
| private setupThemeObserver(): void {
 | |
|   const observer = new MutationObserver(() => {
 | |
|     const darkMode = document.documentElement.classList.contains('dark');
 | |
|     this.isDarkTheme.set(darkMode);
 | |
|     this.reconfigureTheme();
 | |
|   });
 | |
|   observer.observe(document.documentElement, {
 | |
|     attributes: true,
 | |
|     attributeFilter: ['class']
 | |
|   });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 💾 Sauvegarde
 | |
| 
 | |
| ### VaultService Integration
 | |
| 
 | |
| Le composant utilise la méthode privée `saveMarkdown()` du VaultService:
 | |
| 
 | |
| ```typescript
 | |
| async save(): Promise<void> {
 | |
|   const content = this.editorView?.state.doc.toString() || '';
 | |
|   const success = await (this.vaultService as any).saveMarkdown(
 | |
|     this.filePath(),
 | |
|     content
 | |
|   );
 | |
|   
 | |
|   if (success) {
 | |
|     this.isDirty.set(false);
 | |
|     this.editorStateService.markAsSaved();
 | |
|     this.toastService.success('Saved successfully');
 | |
|   } else {
 | |
|     this.toastService.error('Failed to save');
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Autosave
 | |
| 
 | |
| L'autosave est déclenché après 5 secondes d'inactivité:
 | |
| 
 | |
| ```typescript
 | |
| private scheduleAutosave(): void {
 | |
|   if (this.autosaveTimer) {
 | |
|     clearTimeout(this.autosaveTimer);
 | |
|   }
 | |
|   
 | |
|   this.autosaveTimer = setTimeout(() => {
 | |
|     if (this.isDirty() && !this.isSaving()) {
 | |
|       this.save();
 | |
|     }
 | |
|   }, 5000);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## ⌨️ Raccourcis clavier
 | |
| 
 | |
| | Raccourci | Action |
 | |
| |-----------|--------|
 | |
| | `Ctrl/Cmd + S` | Sauvegarder |
 | |
| | `Ctrl/Cmd + F` | Rechercher |
 | |
| | `Ctrl/Cmd + Z` | Annuler |
 | |
| | `Ctrl/Cmd + Y` | Refaire |
 | |
| | `Esc` | Fermer la recherche |
 | |
| | `Tab` | Indenter |
 | |
| | `Shift + Tab` | Désindenter |
 | |
| 
 | |
| ## 📱 Responsive Mobile
 | |
| 
 | |
| ### Toolbar compacte
 | |
| 
 | |
| ```css
 | |
| @media (max-width: 640px) {
 | |
|   .btn-editor {
 | |
|     padding: 0.375rem 0.5rem;
 | |
|   }
 | |
|   
 | |
|   .btn-editor span {
 | |
|     display: none; /* Cache les labels, garde les icônes */
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Éditeur optimisé
 | |
| 
 | |
| ```css
 | |
| @media (max-width: 640px) {
 | |
|   .cm-editor {
 | |
|     font-size: 13px !important;
 | |
|   }
 | |
|   
 | |
|   .cm-content {
 | |
|     padding: 0.75rem;
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🧪 Tests manuels
 | |
| 
 | |
| ### Checklist de validation
 | |
| 
 | |
| - [ ] **Bouton "Éditer"**
 | |
|   - [ ] Visible à gauche de "Open in Full Screen"
 | |
|   - [ ] Alignement correct
 | |
|   - [ ] États hover/active
 | |
|   - [ ] Non visible pour fichiers Excalidraw
 | |
| 
 | |
| - [ ] **Passage en mode édition**
 | |
|   - [ ] Transition fluide sans glitch
 | |
|   - [ ] Contenu chargé correctement
 | |
|   - [ ] Front-matter YAML préservé
 | |
|   - [ ] Cursor positionné
 | |
| 
 | |
| - [ ] **Édition**
 | |
|   - [ ] Coloration syntaxique Markdown
 | |
|   - [ ] Line numbers
 | |
|   - [ ] Word wrap toggle
 | |
|   - [ ] Search (Ctrl+F) fonctionne
 | |
|   - [ ] Undo/Redo fonctionnent
 | |
|   - [ ] Autosave après 5s
 | |
| 
 | |
| - [ ] **Sauvegarde**
 | |
|   - [ ] Ctrl+S sauvegarde
 | |
|   - [ ] Toast "Saved successfully"
 | |
|   - [ ] Flag dirty réinitialisé
 | |
|   - [ ] Erreur I/O gérée (toast d'erreur)
 | |
| 
 | |
| - [ ] **Navigation guard**
 | |
|   - [ ] Confirmation si modifications non sauvegardées
 | |
|   - [ ] Pas de confirmation si sauvegardé
 | |
|   - [ ] beforeunload (fermeture navigateur)
 | |
| 
 | |
| - [ ] **Thème Dark/Light**
 | |
|   - [ ] Basculement dynamique
 | |
|   - [ ] Couleurs correctes
 | |
|   - [ ] Contraste lisible
 | |
| 
 | |
| - [ ] **Responsive Mobile**
 | |
|   - [ ] Toolbar compacte (≤ 390px)
 | |
|   - [ ] Boutons visibles
 | |
|   - [ ] Scroll fluide
 | |
|   - [ ] Clavier n'occulte pas toolbar
 | |
| 
 | |
| - [ ] **Performance**
 | |
|   - [ ] Pas de lag en mode lecture
 | |
|   - [ ] Lazy loading confirmé (devtools Network)
 | |
|   - [ ] Note lourde (>5000 lignes) éditable
 | |
| 
 | |
| ## 🚀 Utilisation
 | |
| 
 | |
| ### Pour l'utilisateur
 | |
| 
 | |
| 1. Ouvrir une note Markdown en mode lecture
 | |
| 2. Cliquer sur le bouton "Éditer" (icône crayon) dans la toolbar
 | |
| 3. L'éditeur CodeMirror 6 se charge
 | |
| 4. Éditer le contenu
 | |
| 5. Sauvegarder avec Ctrl+S ou le bouton "Save"
 | |
| 6. Cliquer sur "Close" pour revenir en mode lecture
 | |
| 
 | |
| ### Pour le développeur
 | |
| 
 | |
| **Ajouter une extension CodeMirror:**
 | |
| 
 | |
| ```typescript
 | |
| // Dans markdown-editor.component.ts, méthode initializeEditor()
 | |
| const initialState = EditorState.create({
 | |
|   extensions: [
 | |
|     // ... extensions existantes
 | |
|     myCustomExtension(),  // Ajouter ici
 | |
|   ]
 | |
| });
 | |
| ```
 | |
| 
 | |
| **Personnaliser le thème:**
 | |
| 
 | |
| Modifier `src/styles/codemirror.css` et ajuster les variables CSS.
 | |
| 
 | |
| **Ajouter un bouton dans la toolbar:**
 | |
| 
 | |
| Éditer le template de `markdown-editor.component.ts`.
 | |
| 
 | |
| ## 🐛 Debugging
 | |
| 
 | |
| ### Console logs utiles
 | |
| 
 | |
| - `[SmartFileViewer] Editor loaded successfully` - Éditeur chargé
 | |
| - `[SmartFileViewer] Editor unloaded` - Éditeur déchargé
 | |
| - `[MarkdownViewer] Cannot edit: no file path` - Pas de path
 | |
| 
 | |
| ### Problèmes courants
 | |
| 
 | |
| **1. L'éditeur ne se charge pas**
 | |
| - Vérifier que `EditorStateService.isEditMode()` est true
 | |
| - Vérifier dans la console les erreurs d'import dynamique
 | |
| - Vérifier que le ViewContainerRef est disponible
 | |
| 
 | |
| **2. Le thème ne change pas**
 | |
| - Vérifier que la classe `dark` est bien sur `<html>`
 | |
| - Vérifier que le MutationObserver est actif
 | |
| - Inspecter les variables CSS dans DevTools
 | |
| 
 | |
| **3. La sauvegarde échoue**
 | |
| - Vérifier le path du fichier
 | |
| - Vérifier les permissions serveur
 | |
| - Vérifier les logs serveur (API `/api/files?path=...`)
 | |
| 
 | |
| ## 📦 Dépendances
 | |
| 
 | |
| ### Packages installés
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "@codemirror/view": "^6.x",
 | |
|   "@codemirror/state": "^6.x",
 | |
|   "@codemirror/language": "^6.x",
 | |
|   "@codemirror/lang-markdown": "^6.x",
 | |
|   "@codemirror/commands": "^6.x",
 | |
|   "@codemirror/search": "^6.x",
 | |
|   "@codemirror/autocomplete": "^6.x",
 | |
|   "@codemirror/lint": "^6.x",
 | |
|   "@codemirror/legacy-modes": "^6.x",
 | |
|   "@lezer/highlight": "^1.x"
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Bundle size impact
 | |
| 
 | |
| - **Mode lecture:** 0 KB (lazy loading)
 | |
| - **Mode édition:** ~150 KB (CodeMirror + extensions)
 | |
| 
 | |
| ## 🔮 Évolutions futures
 | |
| 
 | |
| - [ ] Preview split (côte à côte Markdown/Rendu)
 | |
| - [ ] Formatage rapide (bold, italic, list) via toolbar
 | |
| - [ ] Snippets personnalisés
 | |
| - [ ] Vim mode (via @replit/codemirror-vim)
 | |
| - [ ] Collaborative editing (via Y.js)
 | |
| - [ ] Export PDF depuis l'éditeur
 | |
| - [ ] Statistiques (nombre de mots, temps d'édition)
 | |
| 
 | |
| ## 📝 Notes techniques
 | |
| 
 | |
| ### Pourquoi CodeMirror 6 ?
 | |
| 
 | |
| - Architecture modulaire (extensions)
 | |
| - Performance optimale (virtual scrolling)
 | |
| - TypeScript first
 | |
| - Thème CSS facilement personnalisable
 | |
| - Communauté active
 | |
| 
 | |
| ### Alternatives considérées
 | |
| 
 | |
| - **Monaco Editor** - Trop lourd (2+ MB), conçu pour IDE
 | |
| - **Ace Editor** - API moins moderne, maintenance limitée
 | |
| - **ProseMirror** - Orienté WYSIWYG, pas Markdown brut
 | |
| 
 | |
| ---
 | |
| 
 | |
| **Date de création:** 2025-01-20  
 | |
| **Version:** 1.0.0  
 | |
| **Auteur:** Lead Frontend (Angular 20 + Tailwind)
 |