allow organize pics
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-25 18:49:11 +02:00
parent 895fec2b40
commit 0c7ae46b4d
4 changed files with 129 additions and 29 deletions

View file

@ -1,12 +1,33 @@
// Rôle : fournit l'outil images avec import, collage, libellés et annotation rapide.
import { useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
export function ImagesModule({ toolboxId, moduleId, context, editing }) {
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 {
draggingId: draggingImageId,
dropTarget,
startDrag
} = usePointerReorder({
targetSelector: "[data-image-id]",
getTargetId: (target) => target.dataset.imageId,
canDropOn: (target) => target.dataset.imageModuleId === moduleId,
getPlacement: (event, target) => {
const rect = target.getBoundingClientRect();
return event.clientX > rect.left + rect.width / 2 ? "after" : "before";
},
onMove: (draggingId, targetId, placement) => {
const nextImages = data.images.filter((image) => image.id !== draggingId);
const targetIndex = nextImages.findIndex((image) => image.id === targetId);
if (targetIndex === -1) return;
nextImages.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, data.images.find((image) => image.id === draggingId));
context.setModuleData(toolboxId, moduleId, { images: nextImages.filter(Boolean) });
}
});
async function addFiles(files) {
if (await context.addImageFiles(toolboxId, moduleId, files)) {
@ -68,40 +89,59 @@ export function ImagesModule({ toolboxId, moduleId, context, editing }) {
</div>
)}
<div className="images">
{data.images.map((image) => (
<figure key={image.id}>
<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"
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">
{data.images.map((image) => {
const isDragging = draggingImageId === image.id;
const isDropTarget = dropTarget.id === image.id;
const className = [
isDragging ? "is-dragging" : "",
isDropTarget ? "is-drop-target" : "",
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return (
<figure key={image.id} className={className} data-image-id={image.id} data-image-module-id={moduleId}>
<button
className="image-annotate-button"
className="image-drag-handle"
type="button"
onClick={() => context.createImageAnnotationModule?.(image.dataUrl)}
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
title={textContent.annotateTitle || "Annoter"}
onPointerDown={(event) => startDrag(event, image.id)}
aria-label={textContent.reorderAriaLabel || "Déplacer l'image"}
title={textContent.reorderTitle || "Déplacer"}
>
<Icon name="map" />
<Icon name="drag" />
</button>
<button className="image-delete-button 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 className="image-preview" onClick={() => context.setImage(image)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
<img src={image.dataUrl} alt={textContent.imageAlt || "Image"} />
</button>
</div>
</figure>
))}
{editing ? (
<input
className="image-label-input"
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"
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 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>
</>
);

View file

@ -1720,6 +1720,60 @@ textarea:focus {
background: rgba(5, 7, 17, 0.38);
}
.images figure.is-dragging {
opacity: 0.42;
}
.images figure.is-drop-target {
border-color: rgba(246, 196, 83, 0.72);
box-shadow: 0 0 0 1px rgba(246, 196, 83, 0.26), 0 0 18px rgba(246, 196, 83, 0.12);
}
.images figure.is-drop-target.drop-after {
border-color: rgba(112, 89, 255, 0.82);
box-shadow: 0 0 0 1px rgba(164, 124, 255, 0.3), 0 0 18px rgba(112, 89, 255, 0.16);
}
.image-drag-handle {
position: absolute;
z-index: 4;
top: 12px;
left: 12px;
display: grid;
width: 32px;
height: 32px;
place-items: center;
padding: 0;
border: 1px solid rgba(138, 150, 197, 0.22);
border-radius: var(--radius-md);
background: rgba(9, 13, 29, 0.76);
color: rgba(229, 235, 255, 0.78);
cursor: grab;
opacity: 0;
transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast), box-shadow var(--transition-fast);
}
.image-drag-handle .ui-icon {
width: 16px;
height: 16px;
}
.images figure:hover .image-drag-handle,
.images figure:focus-within .image-drag-handle {
opacity: 1;
}
.image-drag-handle:hover {
border-color: rgba(246, 196, 83, 0.56);
background: rgba(20, 28, 55, 0.86);
color: var(--color-accent-gold);
box-shadow: 0 0 14px rgba(246, 196, 83, 0.12);
}
.image-drag-handle:active {
cursor: grabbing;
}
.image-preview {
display: block;
width: 100%;