docs(BlockEditor): document mediaSlug media library link and add server helpers
- update image block schema in README table to include `mediaSlug?` and clarify fields - add "Liaison avec la médiathèque" section documenting mediaSlug behavior, read-only alt/caption, and internal `_` fields - document new server helpers (`normalizeImageBlocks`, `enrichBlocksWithMedia`, `syncBlockImageReferences`) with usage examples - add `block_image` field convention to media feature README with cross-references - implement `mediaLink.server.js` with the three server-side helpers - store `mediaSlug` on image block at insertion time in `Image.client.js` - persist `mediaSlug` through clipboard paste/duplicate in `clipboard.js` - export `mediaLink` entry point in `package.json` exports map
This commit is contained in:
@@ -30,7 +30,7 @@ Chaque bloc a un `id` (UUID) et un `type`. Selon le type :
|
||||
| `quote` | `content` | citation |
|
||||
| `code` | `content` | bloc de code (monospace) |
|
||||
| `divider` | — | séparateur horizontal |
|
||||
| `image` | `src`, `alt`, `caption`, `align`, `href`, `newTab` | image (URL uniquement) — `align` ∈ `left\|center\|right\|full`, `href` optionnel |
|
||||
| `image` | `src`, `mediaSlug?`, `alt?`, `caption?`, `align`, `href`, `newTab` | image — voir [Bloc image](#bloc-image) ; `align` ∈ `left\|center\|right\|full` |
|
||||
|
||||
`content` est un **tableau `InlineNode[]`** depuis Phase 2 — voir ci-dessous.
|
||||
|
||||
@@ -291,10 +291,75 @@ Le formulaire d'insertion accepte deux sources :
|
||||
|
||||
- **URL externe** : champ libre + bouton « Insérer ».
|
||||
- **Bibliothèque media** : bouton « Choisir un média » qui ouvre le `MediaPicker`
|
||||
de `@zen/core/features/media`. À la sélection, le bloc est inséré avec
|
||||
`block.src = "/zen/api/media/file/<slug>"`. Les nouveaux uploads passent par
|
||||
ce picker en visibilité `public` (les blocs image vivent dans du contenu
|
||||
publié — un média privé serait illisible côté frontal).
|
||||
de `@zen/core/features/media`. À la sélection, le bloc reçoit
|
||||
`block.src = "/zen/api/media/file/<slug>"` **et** `block.mediaSlug = "<slug>"`.
|
||||
Les nouveaux uploads passent par ce picker en visibilité `public` (les blocs
|
||||
image vivent dans du contenu publié — un média privé serait illisible côté
|
||||
frontal).
|
||||
|
||||
### Liaison avec la médiathèque (`mediaSlug`)
|
||||
|
||||
Quand un bloc image est lié à un média (`mediaSlug` présent), le **média est
|
||||
la source unique de vérité** pour `alt` et `caption`. Le bloc ne stocke ni l'un
|
||||
ni l'autre — l'éditeur affiche les valeurs du média en lecture seule avec un
|
||||
lien « Modifier dans la médiathèque ». Toute mise à jour de l'alt ou de la
|
||||
légende côté médiathèque est immédiatement répercutée sur tous les blocs qui
|
||||
référencent ce média, sans toucher aux contenus.
|
||||
|
||||
Pour les **URL externes** (pas de `mediaSlug`), les inputs alt/caption locaux
|
||||
restent disponibles sur le bloc — il n'y a pas d'autre source possible.
|
||||
|
||||
Champs internes au composant (préfixe `_`, jamais persistés) :
|
||||
|
||||
- `_mediaAlt` / `_mediaCaption` : snapshot capté à l'insertion via le
|
||||
`MediaPicker`, pour affichage immédiat dans le bandeau lecture-seule sans
|
||||
appel réseau.
|
||||
- `_resolvedAlt` / `_resolvedCaption` : injectés au rendu serveur par
|
||||
`enrichBlocksWithMedia` (cf. ci-dessous).
|
||||
|
||||
### Helpers serveur — `mediaLink.server.js`
|
||||
|
||||
Trois helpers à appeler depuis les actions/routes serveur du module qui
|
||||
héberge le contenu BlockEditor :
|
||||
|
||||
```js
|
||||
import {
|
||||
normalizeImageBlocks,
|
||||
enrichBlocksWithMedia,
|
||||
syncBlockImageReferences,
|
||||
} from '@zen/core/shared/components/BlockEditor/mediaLink';
|
||||
|
||||
// Au save :
|
||||
const normalized = await normalizeImageBlocks(blocks);
|
||||
await persistDocument(documentId, normalized);
|
||||
await syncBlockImageReferences({
|
||||
sourceType: 'post', // ou 'page', '@zen/module-shop:product', …
|
||||
sourceId: documentId,
|
||||
blocks: normalized,
|
||||
});
|
||||
|
||||
// Au render (page SSR) :
|
||||
const blocks = await loadDocument(slug);
|
||||
const enriched = await enrichBlocksWithMedia(blocks);
|
||||
return <BlockEditor value={enriched} disabled />;
|
||||
```
|
||||
|
||||
`normalizeImageBlocks` :
|
||||
- Lazy upgrade : extrait `mediaSlug` depuis `src` quand l'URL matche
|
||||
`/zen/api/media/file/<slug>`.
|
||||
- Back-fill : si le média n'a pas encore d'`alt_text` / `caption` mais que le
|
||||
bloc en porte (legacy), remonte la valeur côté `zen_media`.
|
||||
- Strict : nettoie `alt` et `caption` du bloc dès qu'un `mediaSlug` est résolu.
|
||||
|
||||
`enrichBlocksWithMedia` : une seule requête batch pour tous les `mediaSlug`
|
||||
du document, attache `_resolvedAlt` / `_resolvedCaption`. Le composant de
|
||||
rendu lit ces champs en priorité.
|
||||
|
||||
`syncBlockImageReferences` : synchronise `zen_media_references` avec
|
||||
`field = 'block_image'`. La contrainte FK `ON DELETE RESTRICT` empêche
|
||||
ensuite la suppression d'un média référencé par un bloc image tant que le
|
||||
document hôte existe. Pensez à appeler `detachAllForSource` (de
|
||||
`@zen/core/features/media`) à la suppression du document hôte.
|
||||
|
||||
Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
|
||||
(coin haut-droit) reprenant le style `BOX_CLASS` partagé :
|
||||
@@ -313,14 +378,18 @@ Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
|
||||
- **Supprimer** : vide tous les champs ; le formulaire URL réapparaît.
|
||||
|
||||
Les inputs *Légende* et *Texte alternatif* sous l'image sont des `<input>`
|
||||
HTML standards. Le clic dans ces inputs ne déclenche pas la sélection
|
||||
multi-blocs (cf. garde dans `handleContainerMouseDown` : tout `input`,
|
||||
`textarea`, `select` ou `[contenteditable="true"]` reçoit son focus normal
|
||||
et n'entre pas en mode sélection).
|
||||
HTML standards (uniquement pour les images sans `mediaSlug` — cf.
|
||||
[Liaison avec la médiathèque](#liaison-avec-la-médiathèque-mediaslug)).
|
||||
Le clic dans ces inputs ne déclenche pas la sélection multi-blocs (cf. garde
|
||||
dans `handleContainerMouseDown` : tout `input`, `textarea`, `select` ou
|
||||
`[contenteditable="true"]` reçoit son focus normal et n'entre pas en mode
|
||||
sélection).
|
||||
|
||||
Sérialisation HTML : `<figure data-align="…"><a href><img></a><figcaption></figcaption></figure>`.
|
||||
Le parser inverse (`htmlToBlocks`) lit `data-align`, et descend dans `<a>`
|
||||
quand l'`<img>` y est imbriqué pour récupérer `href` / `target`.
|
||||
quand l'`<img>` y est imbriqué pour récupérer `href` / `target`. Si le `src`
|
||||
de l'image matche `/zen/api/media/file/<slug>`, il reconstitue `mediaSlug`
|
||||
pour préserver la liaison média lors d'un copier-coller interne.
|
||||
|
||||
## Limitations connues
|
||||
|
||||
|
||||
Reference in New Issue
Block a user