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
|
||||
|
||||
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**
|
||||
(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
|
||||
|
||||
- Pas d'imbrication de listes.
|
||||
- Image : URL uniquement, pas d'upload de fichier. La caption est une
|
||||
string plate (pas de formatage inline pour l'instant).
|
||||
- Image : insertion par URL externe ou via le `MediaPicker` du module
|
||||
`media` (les uploads y sont gérés). La caption est une string plate
|
||||
(pas de formatage inline pour l'instant).
|
||||
- Tables : Phase 3.
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
SEPARATOR_VERTICAL_CLASS,
|
||||
} from '../inline/menuStyles.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
|
||||
// rendue + toolbar flottante (alignement, lien, remplacer, supprimer) +
|
||||
@@ -44,6 +47,7 @@ function getAlign(block) {
|
||||
|
||||
function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
|
||||
const [url, setUrl] = useState(initialUrl ?? '');
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,36 +71,57 @@ function ImageUrlForm({ initialUrl, onSubmit, onCancel, disabled }) {
|
||||
}
|
||||
|
||||
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" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
placeholder="URL de l'image (https://…)"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
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 && (
|
||||
<>
|
||||
<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" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
placeholder="URL de l'image (https://…)"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
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={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"
|
||||
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"
|
||||
>
|
||||
Annuler
|
||||
Insérer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
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