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
|
|
@ -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