ObsiViewer/docs/MARKDOWN_EDITOR.md

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 é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:

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

  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:

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

{
  "@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)