content & style opti, new tool annotation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
This commit is contained in:
parent
0db35a0d2c
commit
6e1472d2fb
21 changed files with 1112 additions and 194 deletions
128
website/src/features/toolboxes/modules/ImageAnnotationModule.jsx
Normal file
128
website/src/features/toolboxes/modules/ImageAnnotationModule.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
function markerLabel(index, marker, textContent) {
|
||||
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
|
||||
}
|
||||
|
||||
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
|
||||
const textContent = context.moduleText?.imageAnnotation || {};
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const markers = data.markers;
|
||||
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData);
|
||||
}
|
||||
|
||||
async function setImageFromFiles(files) {
|
||||
const file = [...files].find((item) => item?.type?.startsWith("image/"));
|
||||
if (!file) return;
|
||||
save({ image: await context.compressImageFile(file), markers: [] });
|
||||
setDragOver(false);
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
save({ image: "", markers: [] });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-annotation-module">
|
||||
{editing && (
|
||||
<div className="module-add-panel">
|
||||
<label
|
||||
className={`dropzone annotation-dropzone ${dragOver ? "is-drag-over" : ""}`}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
setImageFromFiles(event.dataTransfer.files);
|
||||
}}
|
||||
>
|
||||
<input type="file" accept="image/*" hidden onChange={(event) => setImageFromFiles(event.target.files)} />
|
||||
{data.image ? textContent.replaceImage || "Remplacer l'image" : textContent.addImage || "Ajouter une image"}
|
||||
</label>
|
||||
<div
|
||||
className="paste-target"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
aria-label={textContent.pasteAriaLabel || "Coller une image à annoter 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;
|
||||
setImageFromFiles(files);
|
||||
}}
|
||||
>
|
||||
{pastePlaceholder}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.image ? (
|
||||
<div className="annotation-workspace">
|
||||
<div className="annotation-stage annotation-stage-preview" aria-label={textContent.stageAriaLabel || "Image annotée"}>
|
||||
<div className="annotation-media">
|
||||
<img src={data.image} alt={textContent.imageAlt || "Image annotée"} draggable="false" />
|
||||
<div className="annotation-preview-actions">
|
||||
<button
|
||||
className="calculator-action-button"
|
||||
type="button"
|
||||
onClick={() => context.setScreenshot({
|
||||
dataUrl: data.image,
|
||||
markers,
|
||||
alt: textContent.imageAlt || "Image annotée",
|
||||
canAnnotate: true,
|
||||
createMarkerId: () => context.uid("marker"),
|
||||
markerPrefix: textContent.markerPrefix || "Marqueur",
|
||||
addMarkerAriaLabel: textContent.addMarkerAriaLabel || "Ajouter un marqueur sur l'image",
|
||||
markerPlaceholder: textContent.markerPlaceholder || "Libellé du marqueur",
|
||||
deleteMarkerTitle: textContent.deleteMarkerTitle || "Supprimer",
|
||||
onChangeMarkers: (nextMarkers) => save({ ...data, markers: nextMarkers })
|
||||
})}
|
||||
aria-label={textContent.previewAriaLabel || "Agrandir l'image annotée"}
|
||||
title={textContent.previewTitle || "Agrandir"}
|
||||
>
|
||||
<Icon name="zoom" />
|
||||
</button>
|
||||
{editing && (
|
||||
<button className="calculator-action-button danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{markers.map((marker, index) => (
|
||||
<span
|
||||
key={marker.id}
|
||||
className="annotation-marker"
|
||||
style={{ "--marker-x": `${marker.x}%`, "--marker-y": `${marker.y}%` }}
|
||||
title={markerLabel(index, marker, textContent)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="annotation-empty muted">{textContent.emptyImage || "Ajoutez une image pour commencer l'annotation."}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue