new tools modules & sass opti
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
821e8e0a36
commit
9e275e0167
24 changed files with 3266 additions and 2372 deletions
|
|
@ -24,6 +24,9 @@ export function MonsterCard({ monster, t }) {
|
|||
>
|
||||
<div className="mhwilds-card-inner">
|
||||
<div className="mhwilds-card-face mhwilds-card-front">
|
||||
<span className="mhwilds-flip-indicator" aria-hidden="true">
|
||||
<span className="ui-icon ui-icon-flip" />
|
||||
</span>
|
||||
<div className="mhwilds-card-art">
|
||||
<img src={assetPath(monster.name)} alt={t(monster.name, { capitalize: true })} loading="lazy" />
|
||||
</div>
|
||||
|
|
@ -35,6 +38,9 @@ export function MonsterCard({ monster, t }) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="mhwilds-card-face mhwilds-card-back">
|
||||
<span className="mhwilds-flip-indicator" aria-hidden="true">
|
||||
<span className="ui-icon ui-icon-flip" />
|
||||
</span>
|
||||
<div className="mhwilds-card-body">
|
||||
<p className="eyebrow">Détails</p>
|
||||
<h2>{t(monster.name, { capitalize: true })}</h2>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export function ChecklistModule({ toolboxId, moduleId, context }) {
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||
const [label, setLabel] = useState("");
|
||||
const [qty, setQty] = useState(1);
|
||||
|
|
@ -20,11 +20,13 @@ export function ChecklistModule({ toolboxId, moduleId, context }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
||||
<input name="label" placeholder="Nouvel item" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
{editing && (
|
||||
<form className="inline-form checklist-add-form module-add-panel" onSubmit={addItem}>
|
||||
<input name="label" placeholder="Nouvel item" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
)}
|
||||
<ul className="checklist">
|
||||
{data.items.map((item) => (
|
||||
<ChecklistItem key={item.id} item={item} toolboxId={toolboxId} moduleId={moduleId} context={context} items={data.items} save={save} />
|
||||
|
|
|
|||
55
website/src/features/toolboxes/modules/CountersModule.jsx
Normal file
55
website/src/features/toolboxes/modules/CountersModule.jsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||
const [label, setLabel] = useState("");
|
||||
|
||||
function save(counters) {
|
||||
context.setModuleData(toolboxId, moduleId, { counters });
|
||||
}
|
||||
|
||||
function addCounter(event) {
|
||||
event.preventDefault();
|
||||
const cleanLabel = label.trim();
|
||||
if (!cleanLabel) return;
|
||||
|
||||
save([...data.counters, { id: context.uid("counter"), label: cleanLabel, value: 0 }]);
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
function updateCounter(counterId, updater) {
|
||||
save(data.counters.map((counter) => counter.id === counterId ? updater(counter) : counter));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
||||
<input name="label" placeholder="Nom du compteur" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
)}
|
||||
<div className="counters-grid">
|
||||
{data.counters.map((counter) => (
|
||||
<article className="counter-item" key={counter.id}>
|
||||
<div>
|
||||
<strong>{counter.value}</strong>
|
||||
<span>{counter.label}</span>
|
||||
</div>
|
||||
<div className="counter-actions">
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`Décrémenter ${counter.label}`}>-</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`Incrémenter ${counter.label}`}>+</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`Réinitialiser ${counter.label}`} title="Réinitialiser">
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`Supprimer ${counter.label}`} title="Supprimer">
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
website/src/features/toolboxes/modules/LinksModule.jsx
Normal file
67
website/src/features/toolboxes/modules/LinksModule.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||
const [title, setTitle] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [copiedId, setCopiedId] = useState("");
|
||||
|
||||
function save(links) {
|
||||
context.setModuleData(toolboxId, moduleId, { links });
|
||||
}
|
||||
|
||||
function addLink(event) {
|
||||
event.preventDefault();
|
||||
const cleanUrl = context.normalizeUrl(url);
|
||||
if (!cleanUrl) return;
|
||||
|
||||
save([
|
||||
...data.links,
|
||||
{
|
||||
id: context.uid("link"),
|
||||
title: title.trim(),
|
||||
url: cleanUrl
|
||||
}
|
||||
]);
|
||||
setTitle("");
|
||||
setUrl("");
|
||||
}
|
||||
|
||||
async function copyUrl(link) {
|
||||
const copied = await context.copyText(link.url);
|
||||
if (!copied) return;
|
||||
setCopiedId(link.id);
|
||||
window.setTimeout(() => setCopiedId(""), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form links-add-form module-add-panel" onSubmit={addLink}>
|
||||
<input name="title" placeholder="Nom du lien" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder="https://..." value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
)}
|
||||
<ul className="links-list">
|
||||
{data.links.map((link) => (
|
||||
<li className="link-item" key={link.id}>
|
||||
<a href={link.url} target="_blank" rel="noreferrer" title={link.url}>
|
||||
<strong>{link.title || context.hostnameFromUrl(link.url)}</strong>
|
||||
<span>{link.url}</span>
|
||||
</a>
|
||||
<div>
|
||||
<button type="button" onClick={() => copyUrl(link)} aria-label={`Copier ${link.title || link.url}`} title={copiedId === link.id ? "Copié" : "Copier"}>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`Supprimer ${link.title || link.url}`} title="Supprimer">
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export function ScreenshotsModule({ toolboxId, moduleId, context }) {
|
||||
export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.getModuleData(toolboxId, moduleId, { shots: [] });
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
||||
|
|
@ -12,47 +12,51 @@ export function ScreenshotsModule({ toolboxId, moduleId, context }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<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)} />
|
||||
Ajouter des screenshots
|
||||
</label>
|
||||
<div
|
||||
className="paste-target"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
aria-label="Coller une image depuis le presse-papiers"
|
||||
onFocus={(event) => {
|
||||
if (event.currentTarget.textContent.trim() === "Coller une image ici") event.currentTarget.textContent = "";
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = "Coller une image ici";
|
||||
}}
|
||||
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 = "Coller une image ici";
|
||||
addFiles(files);
|
||||
}}
|
||||
>
|
||||
Coller une image ici
|
||||
</div>
|
||||
{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)} />
|
||||
Ajouter des screenshots
|
||||
</label>
|
||||
<div
|
||||
className="paste-target"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
aria-label="Coller une image depuis le presse-papiers"
|
||||
onFocus={(event) => {
|
||||
if (event.currentTarget.textContent.trim() === "Coller une image ici") event.currentTarget.textContent = "";
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = "Coller une image ici";
|
||||
}}
|
||||
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 = "Coller une image ici";
|
||||
addFiles(files);
|
||||
}}
|
||||
>
|
||||
Coller une image ici
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="shots">
|
||||
{data.shots.map((shot) => (
|
||||
<figure key={shot.id}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
||||
|
||||
const MODULE_COMPONENTS = {
|
||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule },
|
||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule },
|
||||
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule }
|
||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
||||
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule, editable: true },
|
||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true }
|
||||
};
|
||||
|
||||
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
|
||||
|
|
@ -13,11 +20,183 @@ export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONEN
|
|||
{ label: module.label, icon: module.icon }
|
||||
]));
|
||||
|
||||
export function AddToolControls({ onAdd }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [quickOpen, setQuickOpen] = useState(false);
|
||||
const controlsRef = useRef(null);
|
||||
const modules = Object.entries(TOOLBOX_MODULES);
|
||||
|
||||
useEffect(() => {
|
||||
if (!quickOpen) return undefined;
|
||||
|
||||
function handlePointerDown(event) {
|
||||
if (!controlsRef.current?.contains(event.target)) setQuickOpen(false);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "Escape") setQuickOpen(false);
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [quickOpen]);
|
||||
|
||||
function addTool(type) {
|
||||
onAdd(type);
|
||||
setOpen(false);
|
||||
setQuickOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="tool-add-controls"
|
||||
ref={controlsRef}
|
||||
onMouseLeave={() => setQuickOpen(false)}
|
||||
onFocusCapture={(event) => {
|
||||
if (event.target.closest(".tool-add-quick-toggle")) setQuickOpen(true);
|
||||
}}
|
||||
onBlurCapture={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) setQuickOpen(false);
|
||||
}}
|
||||
aria-label="Ajouter un outil"
|
||||
>
|
||||
<button
|
||||
className="primary tool-add-modal-button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setQuickOpen(false);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Ajouter un outil
|
||||
</button>
|
||||
<button
|
||||
className={`primary tool-add-quick-toggle ${quickOpen ? "active" : ""}`}
|
||||
type="button"
|
||||
onMouseEnter={() => setQuickOpen(true)}
|
||||
onClick={() => setQuickOpen((value) => !value)}
|
||||
aria-label="Afficher l'ajout rapide"
|
||||
aria-expanded={quickOpen}
|
||||
title="Ajout rapide"
|
||||
>
|
||||
<Icon name="dropdown" />
|
||||
</button>
|
||||
<div className={`tool-quick-add ${quickOpen ? "is-open" : ""}`} aria-label="Ajout rapide">
|
||||
{modules.map(([type, module]) => (
|
||||
<button
|
||||
key={type}
|
||||
className="tool-quick-add-button"
|
||||
type="button"
|
||||
onClick={() => onAdd(type)}
|
||||
aria-label={`Ajouter ${module.label}`}
|
||||
title={module.label}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${module.icon}`} />
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{open && createPortal(
|
||||
<div className="tool-add-modal-root" role="dialog" aria-modal="true" aria-labelledby="tool-add-modal-title">
|
||||
<div className="confirm-backdrop" onClick={() => setOpen(false)} />
|
||||
<section className="confirm-modal tool-add-modal">
|
||||
<header>
|
||||
<h2 id="tool-add-modal-title">Ajouter un outil</h2>
|
||||
<button className="drawer-close-button" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</header>
|
||||
<div className="tool-add-grid">
|
||||
{modules.map(([type, module]) => (
|
||||
<button key={type} className="tool-add-card" type="button" onClick={() => addTool(type)}>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${module.icon}`} />
|
||||
</span>
|
||||
<span>{module.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDelete, onMove }) {
|
||||
const [draggingModuleId, setDraggingModuleId] = useState("");
|
||||
const [dropTarget, setDropTarget] = useState({ id: "", placement: "before" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggingModuleId) return undefined;
|
||||
|
||||
function getDropTarget(event) {
|
||||
const element = document.elementFromPoint(event.clientX, event.clientY);
|
||||
const target = element?.closest?.(".module");
|
||||
if (!target || target.dataset.toolboxId !== toolbox.id || target.dataset.moduleId === draggingModuleId) {
|
||||
return { id: "", placement: "before" };
|
||||
}
|
||||
const rect = target.getBoundingClientRect();
|
||||
return {
|
||||
id: target.dataset.moduleId,
|
||||
placement: event.clientY > rect.top + rect.height / 2 ? "after" : "before"
|
||||
};
|
||||
}
|
||||
|
||||
function handlePointerMove(event) {
|
||||
setDropTarget(getDropTarget(event));
|
||||
}
|
||||
|
||||
function handlePointerUp(event) {
|
||||
const target = getDropTarget(event);
|
||||
if (target.id) onMove(draggingModuleId, target.id, target.placement);
|
||||
setDraggingModuleId("");
|
||||
setDropTarget({ id: "", placement: "before" });
|
||||
}
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp, { once: true });
|
||||
window.addEventListener("pointercancel", handlePointerUp, { once: true });
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
window.removeEventListener("pointercancel", handlePointerUp);
|
||||
};
|
||||
}, [draggingModuleId, onMove, toolbox.id]);
|
||||
|
||||
function startModuleDrag(event, moduleId) {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
setDraggingModuleId(moduleId);
|
||||
setDropTarget({ id: "", placement: "before" });
|
||||
}
|
||||
|
||||
if (moduleColumns === 1) {
|
||||
return (
|
||||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||
{toolbox.modules.map((module) => <ModuleShell key={module.id} toolbox={toolbox} module={module} context={context} onRename={onRename} onDelete={onDelete} onMove={onMove} />)}
|
||||
{toolbox.modules.map((module) => (
|
||||
<ModuleShell
|
||||
key={module.id}
|
||||
toolbox={toolbox}
|
||||
module={module}
|
||||
context={context}
|
||||
draggingModuleId={draggingModuleId}
|
||||
dropTarget={dropTarget}
|
||||
onDragStart={startModuleDrag}
|
||||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -29,77 +208,80 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
|||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||
{columns.map((modules, index) => (
|
||||
<div className="module-column" key={index}>
|
||||
{modules.map((module) => <ModuleShell key={module.id} toolbox={toolbox} module={module} context={context} onRename={onRename} onDelete={onDelete} onMove={onMove} />)}
|
||||
{modules.map((module) => (
|
||||
<ModuleShell
|
||||
key={module.id}
|
||||
toolbox={toolbox}
|
||||
module={module}
|
||||
context={context}
|
||||
draggingModuleId={draggingModuleId}
|
||||
dropTarget={dropTarget}
|
||||
onDragStart={startModuleDrag}
|
||||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ModuleShell({ toolbox, module, context, onRename, onDelete, onMove }) {
|
||||
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete }) {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const Component = definition.Component;
|
||||
const label = definition.label || module.type;
|
||||
|
||||
function handleDragStart(event) {
|
||||
if (event.target.closest("button, input, select, textarea, [contenteditable='true']")) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.currentTarget.classList.add("is-dragging");
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", module.id);
|
||||
}
|
||||
|
||||
function handleDragOver(event) {
|
||||
const fromModuleId = event.dataTransfer.getData("text/plain");
|
||||
if (!fromModuleId || fromModuleId === module.id) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.currentTarget.classList.add("is-drop-target");
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
event.currentTarget.classList.toggle("drop-after", event.clientY > rect.top + rect.height / 2);
|
||||
}
|
||||
|
||||
function handleDrop(event) {
|
||||
const fromModuleId = event.dataTransfer.getData("text/plain");
|
||||
if (!fromModuleId || fromModuleId === module.id) return;
|
||||
event.preventDefault();
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
onMove(fromModuleId, module.id, event.clientY > rect.top + rect.height / 2 ? "after" : "before");
|
||||
}
|
||||
|
||||
function clearDragClasses(event) {
|
||||
event.currentTarget.classList.remove("is-dragging", "is-drop-target", "drop-after");
|
||||
}
|
||||
const [editing, setEditing] = useState(false);
|
||||
const isDragging = draggingModuleId === module.id;
|
||||
const isDropTarget = dropTarget.id === module.id;
|
||||
const className = [
|
||||
"module",
|
||||
isDragging ? "is-dragging" : "",
|
||||
isDropTarget ? "is-drop-target" : "",
|
||||
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
<article
|
||||
className="module"
|
||||
className={className}
|
||||
data-toolbox-id={toolbox.id}
|
||||
data-module-id={module.id}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={clearDragClasses}
|
||||
onDrop={handleDrop}
|
||||
onDragEnd={clearDragClasses}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span className="module-drag-handle" aria-hidden="true" title="Déplacer l'outil" />
|
||||
<button
|
||||
className="module-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => onDragStart(event, module.id)}
|
||||
aria-label={`Déplacer ${module.title || label}`}
|
||||
title="Déplacer"
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
|
||||
</span>
|
||||
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
|
||||
</div>
|
||||
<div>
|
||||
{definition.editable && (
|
||||
<button
|
||||
className={`module-edit-button ${editing ? "active" : ""}`}
|
||||
onClick={() => setEditing((value) => !value)}
|
||||
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
||||
aria-pressed={editing}
|
||||
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
)}
|
||||
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">
|
||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} />
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue