This commit is contained in:
parent
895fec2b40
commit
0c7ae46b4d
4 changed files with 129 additions and 29 deletions
|
|
@ -227,6 +227,8 @@
|
||||||
"labelPlaceholder": "Libellé de l'image",
|
"labelPlaceholder": "Libellé de l'image",
|
||||||
"labelAriaLabel": "Libellé de l'image",
|
"labelAriaLabel": "Libellé de l'image",
|
||||||
"previewAriaLabel": "Agrandir l'image",
|
"previewAriaLabel": "Agrandir l'image",
|
||||||
|
"reorderAriaLabel": "Déplacer l'image",
|
||||||
|
"reorderTitle": "Déplacer",
|
||||||
"annotateAriaLabel": "Annoter l'image",
|
"annotateAriaLabel": "Annoter l'image",
|
||||||
"annotateTitle": "Annoter",
|
"annotateTitle": "Annoter",
|
||||||
"deleteAriaLabel": "Supprimer l'image",
|
"deleteAriaLabel": "Supprimer l'image",
|
||||||
|
|
|
||||||
4
website/public/static/icons/clock.svg
Normal file
4
website/public/static/icons/clock.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 7V12L13.5 14.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 455 B |
|
|
@ -1,12 +1,33 @@
|
||||||
// Rôle : fournit l'outil images avec import, collage, libellés et annotation rapide.
|
// Rôle : fournit l'outil images avec import, collage, libellés et annotation rapide.
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||||
|
|
||||||
export function ImagesModule({ toolboxId, moduleId, context, editing }) {
|
export function ImagesModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
||||||
const textContent = context.moduleText?.images || {};
|
const textContent = context.moduleText?.images || {};
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
|
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) {
|
async function addFiles(files) {
|
||||||
if (await context.addImageFiles(toolboxId, moduleId, files)) {
|
if (await context.addImageFiles(toolboxId, moduleId, files)) {
|
||||||
|
|
@ -68,40 +89,59 @@ export function ImagesModule({ toolboxId, moduleId, context, editing }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="images">
|
<div className="images">
|
||||||
{data.images.map((image) => (
|
{data.images.map((image) => {
|
||||||
<figure key={image.id}>
|
const isDragging = draggingImageId === image.id;
|
||||||
<button className="image-preview" onClick={() => context.setImage(image)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
const isDropTarget = dropTarget.id === image.id;
|
||||||
<img src={image.dataUrl} alt={textContent.imageAlt || "Image"} />
|
const className = [
|
||||||
</button>
|
isDragging ? "is-dragging" : "",
|
||||||
{editing ? (
|
isDropTarget ? "is-drop-target" : "",
|
||||||
<input
|
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
|
||||||
className="image-label-input"
|
].filter(Boolean).join(" ");
|
||||||
value={image.label || ""}
|
|
||||||
placeholder={textContent.labelPlaceholder || "Libellé de l'image"}
|
return (
|
||||||
onChange={(event) => updateImage(image.id, { label: event.target.value })}
|
<figure key={image.id} className={className} data-image-id={image.id} data-image-module-id={moduleId}>
|
||||||
aria-label={textContent.labelAriaLabel || "Libellé de l'image"}
|
|
||||||
/>
|
|
||||||
) : image.label ? (
|
|
||||||
<figcaption className="image-label">{image.label}</figcaption>
|
|
||||||
) : null}
|
|
||||||
<div className="image-actions">
|
|
||||||
<button
|
<button
|
||||||
className="image-annotate-button"
|
className="image-drag-handle"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => context.createImageAnnotationModule?.(image.dataUrl)}
|
onPointerDown={(event) => startDrag(event, image.id)}
|
||||||
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
|
aria-label={textContent.reorderAriaLabel || "Déplacer l'image"}
|
||||||
title={textContent.annotateTitle || "Annoter"}
|
title={textContent.reorderTitle || "Déplacer"}
|
||||||
>
|
>
|
||||||
<Icon name="map" />
|
<Icon name="drag" />
|
||||||
</button>
|
</button>
|
||||||
<button className="image-delete-button danger" type="button" onClick={() => {
|
<button className="image-preview" onClick={() => context.setImage(image)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
||||||
context.setModuleData(toolboxId, moduleId, { images: data.images.filter((item) => item.id !== image.id) });
|
<img src={image.dataUrl} alt={textContent.imageAlt || "Image"} />
|
||||||
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
|
|
||||||
<Icon name="trash" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{editing ? (
|
||||||
</figure>
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1720,6 +1720,60 @@ textarea:focus {
|
||||||
background: rgba(5, 7, 17, 0.38);
|
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 {
|
.image-preview {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue