# #69 - Éditeur mobile natif — Interface tactile optimisée > **Statut :** ✅ Terminé — 100 % implémenté + 24 tests frontend (2026-09-12) > **Effort :** 2-3 jours (réalisé) | **Impact :** 🟢 > **Références :** [Roadmap](../ROADMAP.md) · [Changelog](../../CHANGELOG.md) - **Fichiers clés :** - `frontend/js/mobile-editor.js` — module complet (helpers purs + câblage UI) - `frontend/style.css` — barre flottante, poignée de redimensionnement, mode lecture, bottom sheet - `frontend/index.html` — section « Édition mobile » du guide intégré + lien de navigation - `frontend/locales/{fr,en}.json` — clés `mobile_editor.*` et `help.mobile_editor_*` - `frontend/js/app.js` — appel `initMobileEditor()` au démarrage - `tests/frontend/mobile-editor.test.mjs` — 24 tests JSDOM (helpers + câblage DOM) - `.gitea/workflows/ci.yml` — test ajouté au job `lint` - **Description :** sur téléphone et tablette, l'édition et la lecture s'adaptent au tactile : barre d'outils Markdown flottante, bouton « Coller » persistant, zoom par pincement, hauteur ajustable, gestes de balayage (liens entrants / table des matières) et mode lecture plein écran avec navigation d'un fichier à l'autre. --- ## Architecture ``` mobile-editor.js ├── Helpers purs (testables) clampZoom · detectSwipe · formatChange · insertText │ applyFormatToView · applyFormatToTextarea └── Câblage UI (initMobileEditor) ├── Barre d'outils flottante #me-toolbar (gras/italique/code/liste/lien + A−/A+) ├── Presse-papiers #me-paste (Clipboard API + repli iOS) ├── Zoom pincement touchstart/move (2 doigts → taille de police) ├── Hauteur ajustable #me-resize-handle (poignée, persistée) ├── Bottom sheet #me-sheet (liens entrants / table des matières) ├── Raccourcis swipe #content-area (gauche → backlinks, droite → TOC) └── Mode lecture #me-reading-btn (plein écran + navigation fichiers) ``` ## 1. Barre d'outils mobile flottante Injectée dans `#editor-modal` et affichée uniquement sous `768px` lorsque l'éditeur est actif (`.editor-modal.active .me-toolbar`). Chaque bouton applique une transformation Markdown à la sélection courante de CodeMirror (ou du `textarea` de secours) : | Bouton | Action | Transformation | |---|---|---| | **B** | `bold` | `**texte**` | | *I* | `italic` | `*texte*` | | `<>` | `code` | `` `texte` `` | | • | `list` | préfixe `- ` sur chaque ligne sélectionnée | | 🔗 | `link` | `[texte](url)` avec le curseur placé sur `url` | - **Toggle** : ré-appuyer sur un bouton alors que la sélection porte déjà les marqueurs retire ces marqueurs (ex. `**hi**` sélectionné → `hi`). - **Sans sélection** : les marqueurs sont insérés et le curseur est placé à l'intérieur. - Les helpers `formatChange()` (calcul de la transaction) et `applyFormatToView()` / `applyFormatToTextarea()` (application) sont purs côté calcul → testables sans CodeMirror. ## 2. Presse-papiers — bouton « Coller » persistant iOS Safari n'autorise `navigator.clipboard.readText()` que sur geste utilisateur explicite et peut le refuser. Le bouton `📋` : 1. tente `navigator.clipboard.readText()` ; 2. en cas de refus ou d'absence, affiche un message d'aide (`mobile_editor.paste_hint`) invitant à appuyer longuement dans le champ puis à choisir « Coller » ; 3. en cas de succès, insère le texte au niveau du curseur (CodeMirror ou textarea). ## 3. Adaptation CodeMirror - **Zoom par pincement** : deux doigts sur `#editor-body` calculent le ratio de distance et ajustent la taille de police du `.cm-scroller` (bornée `10–28px`). Boutons `A−`/`A+` pour les utilisateurs sans geste tactile. - **Hauteur ajustable** : une poignée (`#me-resize-handle`) entre le corps de l'éditeur et le pied permet d'étirer/réduire la zone d'édition (`flex: 0 0 `), bornée à `140px`. - Les deux valeurs sont **persistées** dans `localStorage` (`obsigate-editor-font-size`, `obsigate-editor-height`) et réappliquées à chaque ouverture / changement de fichier via un `MutationObserver`. ## 4. Raccourcis swipe (lecture) Sur `#content-area`, un `touchstart`/`touchend` unique détecte un geste horizontal (`detectSwipe`, seuil `60px`, dominance horizontale requise) : - **Balayage gauche** → bottom sheet des **liens entrants** (fetch `/api/file/{vault}/backlinks`, clic → ouverture dans un onglet) ; - **Balayage droite** → bottom sheet de la **table des matières** (`state.headingsCache`, clic → `scrollIntoView`). ## 5. Mode lecture plein écran Le bouton flottant `📖` (`#me-reading-btn`) bascule `body.reading-mode` : - masque l'en-tête, la sidebar, la barre latérale droite, la barre d'outils mobile et les actions de fichier ; centre le contenu en colonne de lecture ; - demande l'API **Fullscreen** (repli silencieux si non supportée, ex. iOS) ; la sortie du plein écran (Échap) resynchronise la classe ; - en mode lecture, le **balayage horizontal** navigue vers le fichier **suivant/précédent** du même dossier (`/api/vault/{vault}/files?dir=…&recursive=false`, tri alphabétique, bouclage borné avec message si extrémité atteinte) ; - l'état est mémorisé (`obsigate-reading-mode`) et restauré au rechargement sur mobile. **Visibilité conditionnelle (BUG-016).** Le bouton `📖` n'est affiché que si un **fichier est ouvert** (`state.currentPath`) **et** que le panneau assistant est fermé : sinon il passait au-dessus du panneau mobile plein écran et masquait le bouton d'envoi. `updateReadingButtonVisibility()` est appelé à l'init, à chaque mutation de `#content-area` (MutationObserver) et sur les événements `bookslm:opened` / `bookslm:closed`. --- ## Tests `tests/frontend/mobile-editor.test.mjs` (JSDOM, 24 tests) couvre : - `FORMAT_ACTIONS`, `clampZoom`, `detectSwipe` (seuil, dominance verticale, gauche/droite) ; - `formatChange` pour bold/italic/code/liste/lien, avec et sans sélection, toggle et ligne unique ; - `insertText`, `applyFormatToTextarea`, `applyEditorFontSize` (clamp + persistance) ; - le câblage DOM : construction de `#me-toolbar`, idempotence, contrôles du mode lecture, bascule `toggleReadingMode`/`isReadingMode`, et visibilité conditionnelle de `#me-reading-btn` (fichier ouvert + assistant fermé). Les chemins réseau (backlinks, fichiers adjacents) et les gestes réels restent à valider en E2E navigateur / test manuel mobile. ## Notes - Le module n'importe `ui.js`, `auth.js` et `viewer.js` que **dynamiquement** dans les fonctions concernées, afin de rester testable en JSDOM sans charger la pile applicative complète. - Aucune modification backend : les endpoints réutilisés (`/backlinks`, `/vault/{vault}/files`) existaient déjà.