155 lines
6.5 KiB
JavaScript
155 lines
6.5 KiB
JavaScript
// Rôle : fournit l'outil images avec import fichier/texte, collage, libellés et annotation rapide.
|
|
import { useState } from "react";
|
|
import { Icon } from "../../../components/Icon.jsx";
|
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
|
|
|
export function ImagesModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
|
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
|
const textContent = context.moduleText?.images || {};
|
|
const [dragOver, setDragOver] = useState(false);
|
|
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
|
|
const {
|
|
itemReorder,
|
|
getItemProps,
|
|
isItemDragging,
|
|
isItemDropTarget,
|
|
getDropPlacement
|
|
} = useGroupedReorder({
|
|
namespace: "images",
|
|
items: data.images,
|
|
getItemId: (image) => image.id,
|
|
getParentId: () => moduleId,
|
|
orientation: "horizontal",
|
|
onItemMove: (operation) => {
|
|
context.setModuleData(toolboxId, moduleId, {
|
|
images: moveItem(data.images, operation.sourceId, operation.targetId, operation.placement)
|
|
});
|
|
},
|
|
hierarchy: { enabled: true, stickyParents: true }
|
|
});
|
|
|
|
async function addFiles(files) {
|
|
if (await context.addImageFiles(toolboxId, moduleId, files)) {
|
|
setDragOver(false);
|
|
}
|
|
}
|
|
|
|
function updateImage(imageId, patch) {
|
|
context.setModuleData(toolboxId, moduleId, {
|
|
images: data.images.map((image) => image.id === imageId ? { ...image, ...patch } : image)
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{editing && (
|
|
<div className="module-add-panel">
|
|
<label
|
|
className={`dropzone ${dragOver ? "is-drag-over" : ""}`}
|
|
onDragOver={(event) => {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = "copy";
|
|
setDragOver(true);
|
|
}}
|
|
onDragLeave={() => setDragOver(false)}
|
|
onDrop={(event) => {
|
|
event.preventDefault();
|
|
addFiles(event.dataTransfer.files);
|
|
}}
|
|
>
|
|
<input type="file" accept="image/*" multiple hidden onChange={(event) => addFiles(event.target.files)} />
|
|
{textContent.addImages || "Ajouter des images"}
|
|
</label>
|
|
<div
|
|
className="paste-target"
|
|
contentEditable
|
|
suppressContentEditableWarning
|
|
role="textbox"
|
|
aria-label={textContent.pasteAriaLabel || "Coller une image depuis le presse-papiers"}
|
|
onFocus={(event) => {
|
|
if (event.currentTarget.textContent.trim() === pastePlaceholder) event.currentTarget.textContent = "";
|
|
}}
|
|
onBlur={(event) => {
|
|
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = pastePlaceholder;
|
|
}}
|
|
onPaste={(event) => {
|
|
const files = [...(event.clipboardData?.items || [])]
|
|
.filter((item) => item.type.startsWith("image/"))
|
|
.map((item) => item.getAsFile())
|
|
.filter(Boolean);
|
|
if (!files.length) return;
|
|
event.preventDefault();
|
|
event.currentTarget.textContent = pastePlaceholder;
|
|
addFiles(files);
|
|
}}
|
|
>
|
|
{pastePlaceholder}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{textExchangeOpen && (
|
|
<div className="module-text-exchange-panel">
|
|
<TextExchangeActions type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
|
|
</div>
|
|
)}
|
|
<div className="images">
|
|
{data.images.map((image) => {
|
|
const className = [
|
|
isItemDragging(image.id) ? "is-dragging" : "",
|
|
isItemDropTarget(image.id) ? "is-drop-target" : "",
|
|
getDropPlacement("item", image.id) === "after" ? "drop-after" : ""
|
|
].filter(Boolean).join(" ");
|
|
|
|
return (
|
|
<figure key={image.id} className={className} {...getItemProps({ itemId: image.id, parentId: moduleId })}>
|
|
<button
|
|
className="image-drag-handle nds-icon-button"
|
|
data-size="sm"
|
|
data-variant="ghost"
|
|
type="button"
|
|
onPointerDown={(event) => itemReorder.startDrag(event, image.id)}
|
|
aria-label={textContent.reorderAriaLabel || "Déplacer l'image"}
|
|
title={textContent.reorderTitle || "Déplacer"}
|
|
>
|
|
<Icon name="drag" />
|
|
</button>
|
|
<button className="image-preview" onClick={() => context.setImage(image)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
|
<img src={image.dataUrl} alt={textContent.imageAlt || "Image"} />
|
|
</button>
|
|
{editing ? (
|
|
<input
|
|
className="image-label-input nds-inline-input"
|
|
type="text"
|
|
value={image.label || ""}
|
|
placeholder={textContent.labelPlaceholder || "Libellé de l'image"}
|
|
onChange={(event) => updateImage(image.id, { label: event.target.value })}
|
|
aria-label={textContent.labelAriaLabel || "Libellé de l'image"}
|
|
/>
|
|
) : image.label ? (
|
|
<figcaption className="image-label">{image.label}</figcaption>
|
|
) : null}
|
|
<div className="image-actions">
|
|
<button
|
|
className="image-annotate-button nds-icon-button"
|
|
data-size="sm"
|
|
type="button"
|
|
onClick={() => context.createImageAnnotationModule?.(image.dataUrl)}
|
|
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
|
|
title={textContent.annotateTitle || "Annoter"}
|
|
>
|
|
<Icon name="map" />
|
|
</button>
|
|
<button className="image-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => {
|
|
context.setModuleData(toolboxId, moduleId, { images: data.images.filter((item) => item.id !== image.id) });
|
|
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
|
|
<Icon name="trash" />
|
|
</button>
|
|
</div>
|
|
</figure>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|