This commit is contained in:
parent
b8d1829bc7
commit
d359fd0a35
25 changed files with 1340 additions and 1023 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
|
|
@ -15,11 +15,53 @@ const MODULE_COMPONENTS = {
|
|||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true }
|
||||
};
|
||||
|
||||
const MODULE_COLUMN_GAP_PX = 16;
|
||||
|
||||
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
|
||||
type,
|
||||
{ label: module.label, icon: module.icon }
|
||||
]));
|
||||
|
||||
function getSequentialModuleColumns(modules, splitIndex) {
|
||||
return [
|
||||
modules.slice(0, splitIndex),
|
||||
modules.slice(splitIndex)
|
||||
];
|
||||
}
|
||||
|
||||
function getColumnHeight(heights) {
|
||||
if (!heights.length) return 0;
|
||||
return heights.reduce((total, height) => total + height, 0) + MODULE_COLUMN_GAP_PX * (heights.length - 1);
|
||||
}
|
||||
|
||||
function chooseMeasuredSplitIndex(modules, moduleHeights, fallbackSplitIndex) {
|
||||
if (modules.length <= 1) return modules.length;
|
||||
if (modules.some((module) => !moduleHeights.get(module.id))) return fallbackSplitIndex;
|
||||
|
||||
let bestSplitIndex = fallbackSplitIndex;
|
||||
let bestDiff = Number.POSITIVE_INFINITY;
|
||||
let bestLeftDominantSplitIndex = 0;
|
||||
let bestLeftDominantDiff = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (let splitIndex = 1; splitIndex < modules.length; splitIndex += 1) {
|
||||
const leftHeight = getColumnHeight(modules.slice(0, splitIndex).map((module) => moduleHeights.get(module.id)));
|
||||
const rightHeight = getColumnHeight(modules.slice(splitIndex).map((module) => moduleHeights.get(module.id)));
|
||||
const diff = Math.abs(leftHeight - rightHeight);
|
||||
|
||||
if (leftHeight >= rightHeight && diff < bestLeftDominantDiff) {
|
||||
bestLeftDominantDiff = diff;
|
||||
bestLeftDominantSplitIndex = splitIndex;
|
||||
}
|
||||
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestSplitIndex = splitIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return bestLeftDominantSplitIndex || bestSplitIndex;
|
||||
}
|
||||
|
||||
export function AddToolControls({ onAdd }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [quickOpen, setQuickOpen] = useState(false);
|
||||
|
|
@ -134,6 +176,48 @@ export function AddToolControls({ onAdd }) {
|
|||
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDelete, onMove }) {
|
||||
const [draggingModuleId, setDraggingModuleId] = useState("");
|
||||
const [dropTarget, setDropTarget] = useState({ id: "", placement: "before" });
|
||||
const [measuredSplitIndex, setMeasuredSplitIndex] = useState(() => Math.ceil(toolbox.modules.length / 2));
|
||||
const moduleElementsRef = useRef(new Map());
|
||||
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
||||
|
||||
useEffect(() => {
|
||||
setMeasuredSplitIndex(Math.ceil(toolbox.modules.length / 2));
|
||||
}, [toolbox.id, moduleIdSignature, toolbox.modules.length]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (moduleColumns !== 2 || toolbox.modules.length <= 1) return undefined;
|
||||
|
||||
let frameId = 0;
|
||||
function rebalanceColumns() {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(() => {
|
||||
const moduleHeights = new Map();
|
||||
toolbox.modules.forEach((module) => {
|
||||
const element = moduleElementsRef.current.get(module.id);
|
||||
if (element) moduleHeights.set(module.id, element.getBoundingClientRect().height);
|
||||
});
|
||||
|
||||
setMeasuredSplitIndex((currentSplitIndex) => {
|
||||
const fallbackSplitIndex = Math.min(Math.max(currentSplitIndex, 1), toolbox.modules.length - 1);
|
||||
const nextSplitIndex = chooseMeasuredSplitIndex(toolbox.modules, moduleHeights, fallbackSplitIndex);
|
||||
return nextSplitIndex === currentSplitIndex ? currentSplitIndex : nextSplitIndex;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(rebalanceColumns);
|
||||
|
||||
toolbox.modules.forEach((module) => {
|
||||
const element = moduleElementsRef.current.get(module.id);
|
||||
if (element) observer.observe(element);
|
||||
});
|
||||
rebalanceColumns();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [moduleColumns, toolbox.id, moduleIdSignature, toolbox.modules.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggingModuleId) return undefined;
|
||||
|
|
@ -180,6 +264,14 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
|||
setDropTarget({ id: "", placement: "before" });
|
||||
}
|
||||
|
||||
function registerModuleElement(moduleId, element) {
|
||||
if (element) {
|
||||
moduleElementsRef.current.set(moduleId, element);
|
||||
} else {
|
||||
moduleElementsRef.current.delete(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleColumns === 1) {
|
||||
return (
|
||||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||
|
|
@ -195,14 +287,15 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
|||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
registerModuleElement={registerModuleElement}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [[], []];
|
||||
toolbox.modules.forEach((module, index) => columns[index % 2].push(module));
|
||||
const splitIndex = Math.min(Math.max(measuredSplitIndex, 1), Math.max(1, toolbox.modules.length - 1));
|
||||
const columns = getSequentialModuleColumns(toolbox.modules, splitIndex);
|
||||
|
||||
return (
|
||||
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
|
||||
|
|
@ -220,6 +313,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
|||
onRename={onRename}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
registerModuleElement={registerModuleElement}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -228,7 +322,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
|||
);
|
||||
}
|
||||
|
||||
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete }) {
|
||||
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete, registerModuleElement }) {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const Component = definition.Component;
|
||||
const label = definition.label || module.type;
|
||||
|
|
@ -247,6 +341,7 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
|
|||
className={className}
|
||||
data-toolbox-id={toolbox.id}
|
||||
data-module-id={module.id}
|
||||
ref={(element) => registerModuleElement?.(module.id, element)}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
|
|
@ -273,7 +368,7 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
|
|||
aria-pressed={editing}
|
||||
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
<Icon name="add" />
|
||||
</button>
|
||||
)}
|
||||
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue