feat(ui): add MediaPicker integration to BlockEditor image block
- import MediaPicker from features/media to avoid circular dependency with @zen/core - add "Choisir un média" button in ImageUrlForm alongside existing URL input - insert image block with `/zen/api/media/file/<slug>` src on media selection - update README to document dual-source form (external URL + media library) and revise known limitations
This commit is contained in:
@@ -287,6 +287,15 @@ descendants. Les styles CSS inline (`font-weight`, `font-style`,
|
|||||||
|
|
||||||
## Bloc image
|
## Bloc image
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
|
Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
|
||||||
(coin haut-droit) reprenant le style `BOX_CLASS` partagé :
|
(coin haut-droit) reprenant le style `BOX_CLASS` partagé :
|
||||||
|
|
||||||
@@ -316,6 +325,7 @@ quand l'`<img>` y est imbriqué pour récupérer `href` / `target`.
|
|||||||
## Limitations connues
|
## Limitations connues
|
||||||
|
|
||||||
- Pas d'imbrication de listes.
|
- Pas d'imbrication de listes.
|
||||||
- Image : URL uniquement, pas d'upload de fichier. La caption est une
|
- Image : insertion par URL externe ou via le `MediaPicker` du module
|
||||||
string plate (pas de formatage inline pour l'instant).
|
`media` (les uploads y sont gérés). La caption est une string plate
|
||||||
|
(pas de formatage inline pour l'instant).
|
||||||
- Tables : Phase 3.
|
- Tables : Phase 3.
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import {
|
|||||||
SEPARATOR_VERTICAL_CLASS,
|
SEPARATOR_VERTICAL_CLASS,
|
||||||
} from '../inline/menuStyles.js';
|
} from '../inline/menuStyles.js';
|
||||||
import { newBlockId } from '../utils/ids.js';
|
import { newBlockId } from '../utils/ids.js';
|
||||||
|
// Import relatif pour éviter une dépendance circulaire avec @zen/core (le bloc
|
||||||
|
// image vit dans `shared` et tire MediaPicker depuis `features/media`).
|
||||||
|
import MediaPicker from '../../../../features/media/components/MediaPicker.client.js';
|
||||||
|
|
||||||
// Bloc image. État vide = formulaire d'insertion d'URL. État rempli = image
|
// Bloc image. État vide = formulaire d'insertion d'URL. État rempli = image
|
||||||
// rendue + toolbar flottante (alignement, lien, remplacer, supprimer) +
|
// rendue + toolbar flottante (alignement, lien, remplacer, supprimer) +
|
||||||
@@ -44,6 +47,7 @@ function getAlign(block) {
|
|||||||
|
|
||||||
function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
|
function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
|
||||||
const [url, setUrl] = useState(initialUrl ?? '');
|
const [url, setUrl] = useState(initialUrl ?? '');
|
||||||
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -67,36 +71,57 @@ function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/40 px-3 py-3">
|
<>
|
||||||
<Image01Icon width={18} height={18} className="text-neutral-500" />
|
<div className="flex items-center gap-2 rounded-lg border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/40 px-3 py-3">
|
||||||
<input
|
<Image01Icon width={18} height={18} className="text-neutral-500" />
|
||||||
ref={inputRef}
|
<input
|
||||||
type="url"
|
ref={inputRef}
|
||||||
placeholder="URL de l'image (https://…)"
|
type="url"
|
||||||
value={url}
|
placeholder="URL de l'image (https://…)"
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
value={url}
|
||||||
onKeyDown={handleKeyDown}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
disabled={disabled}
|
onKeyDown={handleKeyDown}
|
||||||
className="flex-1 px-2 py-1 text-sm rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white outline-none focus:border-blue-500"
|
disabled={disabled}
|
||||||
/>
|
className="flex-1 px-2 py-1 text-sm rounded border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white outline-none focus:border-blue-500"
|
||||||
<button
|
/>
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
disabled={disabled || !url.trim()}
|
|
||||||
className="px-3 py-1 text-sm rounded bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 text-white"
|
|
||||||
>
|
|
||||||
Insérer
|
|
||||||
</button>
|
|
||||||
{onCancel && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={submit}
|
||||||
className="px-2 py-1 text-sm rounded text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700/60"
|
disabled={disabled || !url.trim()}
|
||||||
|
className="px-3 py-1 text-sm rounded bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 text-white"
|
||||||
>
|
>
|
||||||
Annuler
|
Insérer
|
||||||
</button>
|
</button>
|
||||||
)}
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
onClick={() => setPickerOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-3 py-1 text-sm rounded border border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700/60 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Choisir un média
|
||||||
|
</button>
|
||||||
|
{onCancel && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-2 py-1 text-sm rounded text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700/60"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MediaPicker
|
||||||
|
isOpen={pickerOpen}
|
||||||
|
onClose={() => setPickerOpen(false)}
|
||||||
|
onSelect={(media) => {
|
||||||
|
if (media?.slug) onSubmit?.(`/zen/api/media/file/${media.slug}`);
|
||||||
|
}}
|
||||||
|
accept="image/*"
|
||||||
|
visibility="any"
|
||||||
|
uploadVisibility="public"
|
||||||
|
title="Insérer une image depuis la bibliothèque"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user