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)
 |