12 KiB
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é:
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 éditionexitEditMode()- Quitte le mode éditionsetDirty(isDirty)- Marque le contenu comme modifiéupdateContent(content)- Met à jour le contenumarkAsSaved()- Réinitialise le flag dirty après sauvegardecanExit()- 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 fichierinitialContent: 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:
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:
{
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:
: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>:
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:
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é:
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
@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é
@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
- Ouvrir une note Markdown en mode lecture
- Cliquer sur le bouton "Éditer" (icône crayon) dans la toolbar
- L'éditeur CodeMirror 6 se charge
- Éditer le contenu
- Sauvegarder avec Ctrl+S ou le bouton "Save"
- Cliquer sur "Close" pour revenir en mode lecture
Pour le développeur
Ajouter une extension CodeMirror:
// 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
darkest 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
{
"@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)