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