131 lines
6.8 KiB
Markdown
131 lines
6.8 KiB
Markdown
# #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 <px>`), 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à.
|