diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 062901f..3ca3a73 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -257,19 +257,18 @@ export function validateSiteContent(site) { "toolboxes.modules.calculator.labelLabel", "toolboxes.modules.calculator.labelPlaceholder", "toolboxes.modules.calculator.saveButton", + "toolboxes.modules.calculator.updateButton", + "toolboxes.modules.calculator.newButton", "toolboxes.modules.calculator.rootButton", "toolboxes.modules.calculator.resetTitle", "toolboxes.modules.calculator.copyTitle", "toolboxes.modules.calculator.copiedTitle", "toolboxes.modules.calculator.scrollableTitle", "toolboxes.modules.calculator.emptyResults", - "toolboxes.modules.calculator.formulaTitle", - "toolboxes.modules.calculator.childFormulaHelp", - "toolboxes.modules.calculator.rootFormulaHelp", "toolboxes.modules.calculator.baseTokenLabel", "toolboxes.modules.calculator.insertBaseTitle", - "toolboxes.modules.calculator.useEntryTitle", - "toolboxes.modules.calculator.renameTitle", + "toolboxes.modules.calculator.editTitle", + "toolboxes.modules.calculator.addChildTitle", "toolboxes.modules.calculator.deleteTitle", "toolboxes.modules.goalTree.editorTitle", "toolboxes.modules.goalTree.treeTitle", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index c16ec47..a2aaae7 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -245,18 +245,23 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /calculateExpression/); assert.match(calculatorModule, /activeParentId/); assert.match(calculatorModule, /parentId/); - assert.match(calculatorModule, /useInlineEdit/); + assert.match(calculatorModule, /MixTagsInput/); assert.match(calculatorModule, /useGroupedReorder/); assert.match(calculatorModule, /calculator-drag-handle/); assert.match(calculatorModule, /scrollResults/); assert.match(calculatorModule, /tool-split-scroll-toggle/); - assert.match(calculatorModule, /EditableCalculatorLabel/); - assert.match(calculatorModule, /tool-split-entry-label/); assert.match(calculatorModule, /copyChecklistImport/); assert.match(calculatorModule, /resetCalculator/); assert.match(calculatorModule, /returnToRoot/); - assert.match(calculatorModule, /setExpression\(activeParent \? formatResult\(activeParent\.value\) : ""\)/); - assert.match(calculatorModule, /EditableCalculatorFormula/); + assert.match(calculatorModule, /createChildEntry/); + assert.match(calculatorModule, /const childId = context\.uid\("calc"\)/); + assert.match(calculatorModule, /parentId: entry\.id/); + assert.match(calculatorModule, /formula: BASE_TOKEN/); + assert.match(calculatorModule, /pendingExpressionFocusRef/); + assert.match(calculatorModule, /setExpression\(BASE_TOKEN\)/); + assert.match(calculatorModule, /editingEntryId/); + assert.match(calculatorModule, /updateButton/); + assert.match(calculatorModule, /onAddChild/); assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/); assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /--calculator-scroll-height/); diff --git a/website/public/data/site.json b/website/public/data/site.json index b57c235..322bc11 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -1854,19 +1854,18 @@ "labelLabel": "Libellé", "labelPlaceholder": "ex: Lingots de fer", "saveButton": "Enregistrer", + "updateButton": "Mettre à jour", + "newButton": "Nouveau calcul", "rootButton": "Revenir à la racine", "resetTitle": "Réinitialiser", "copyTitle": "Copier pour checklist", "copiedTitle": "Copié", "scrollableTitle": "Liste scrollable", "emptyResults": "Aucun résultat enregistré.", - "formulaTitle": "Modifier la formule", - "childFormulaHelp": "Utilisez base pour référencer le parent.", - "rootFormulaHelp": "Valeur ou formule racine.", "baseTokenLabel": "Valeur du parent", "insertBaseTitle": "Ajouter la valeur du parent", - "useEntryTitle": "Utiliser comme base", - "renameTitle": "Renommer", + "editTitle": "Modifier", + "addChildTitle": "Ajouter un calcul enfant", "deleteTitle": "Supprimer" }, "goalTree": { diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx index d78dbbb..f910c16 100644 --- a/website/src/features/toolboxes/modules/CalculatorModule.jsx +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -1,8 +1,7 @@ // Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte. -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { MixTagsInput } from "../../../components/MixTagsInput.jsx"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; @@ -117,6 +116,23 @@ function FormattedCalculatorNumber({ value }) { }); } +function focusExpressionInputAtEnd(element) { + if (!element) return; + element.focus(); + if (typeof element.setSelectionRange === "function") { + const length = element.value?.length || 0; + element.setSelectionRange(length, length); + return; + } + if (!element.isContentEditable) return; + const range = document.createRange(); + range.selectNodeContents(element); + range.collapse(false); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); +} + export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) { const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]); @@ -125,11 +141,17 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe const [expression, setExpression] = useState(""); const [label, setLabel] = useState(""); const [activeParentId, setActiveParentId] = useState(""); + const [editingEntryId, setEditingEntryId] = useState(""); const [calculatorHeight, setCalculatorHeight] = useState(0); const [copied, setCopied] = useState(false); const calculatorCardRef = useRef(null); - const activeParent = calculatedEntries.find((entry) => entry.id === activeParentId); - const result = useMemo(() => calculateExpression(expression, activeParent?.value), [activeParent?.value, expression]); + const expressionInputRef = useRef(null); + const pendingExpressionFocusRef = useRef(false); + const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId); + const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId; + const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId); + const formMode = editingEntry ? "edit" : formulaParent ? "child" : "root"; + const result = useMemo(() => calculateExpression(expression, formulaParent?.value), [formulaParent?.value, expression]); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", @@ -156,6 +178,12 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe return () => observer.disconnect(); }, []); + useLayoutEffect(() => { + if (!pendingExpressionFocusRef.current || !editingEntryId) return; + pendingExpressionFocusRef.current = false; + window.requestAnimationFrame(() => focusExpressionInputAtEnd(expressionInputRef.current)); + }, [editingEntryId]); + function save(entries) { context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) }); } @@ -168,24 +196,58 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe event.preventDefault(); if (result == null) return; const cleanLabel = label.trim(); + const storedFormula = createStoredFormula(expression, formulaParent?.value); + + if (editingEntry) { + save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? { + ...entry, + label: cleanLabel, + value: result, + formula: storedFormula + } : entry)); + setLabel(cleanLabel); + return; + } save([ ...data.entries, { id: context.uid("calc"), - parentId: activeParentId, + parentId: formulaParentId, label: cleanLabel, value: result, - formula: createStoredFormula(expression, activeParent?.value) + formula: storedFormula } ]); - setExpression(activeParent ? formatResult(activeParent.value) : ""); + setExpression(formulaParent ? BASE_TOKEN : ""); setLabel(""); } - function useEntry(entry) { - setActiveParentId(entry.id); + function editEntry(entry) { + setEditingEntryId(entry.id); + setActiveParentId(""); + setExpression(getEditableFormula(entry)); + setLabel(entry.label || ""); + } + + function createChildEntry(entry) { + const childId = context.uid("calc"); + save([ + ...data.entries, + { + id: childId, + parentId: entry.id, + label: "", + value: Number.isFinite(entry.value) ? entry.value : 0, + formula: BASE_TOKEN + } + ]); + setEditingEntryId(""); + pendingExpressionFocusRef.current = true; + setEditingEntryId(childId); + setActiveParentId(""); setExpression(BASE_TOKEN); + setLabel(""); } function deleteEntry(entryId) { @@ -194,34 +256,26 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setActiveParentId(""); setExpression(""); } + if (deletedIds.has(editingEntryId)) { + setEditingEntryId(""); + setLabel(""); + setExpression(""); + } save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); } - function renameEntry(entryId, label) { - const cleanLabel = label.trim(); - save(calculatedEntries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry)); - } - - function updateEntryFormula(entryId, formula) { - const nextEntries = calculatedEntries.map((entry) => { - if (entry.id !== entryId) return entry; - const parent = calculatedEntries.find((candidate) => candidate.id === entry.parentId); - const nextFormula = createStoredFormula(formula, parent?.value); - const nextValue = calculateExpression(nextFormula, parent?.value) ?? entry.value; - return { ...entry, formula: nextFormula, value: nextValue }; - }); - save(nextEntries); - } - function resetCalculator() { setExpression(""); setLabel(""); setActiveParentId(""); + setEditingEntryId(""); } function returnToRoot() { setActiveParentId(""); + setEditingEntryId(""); setExpression(""); + setLabel(""); } async function copyChecklistImport() { @@ -245,10 +299,11 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
{textContent.emptyResults || "Aucun résultat enregistré."}
)} @@ -338,31 +394,15 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe ); } -function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, reorder, onUse, onRename, onUpdateFormula, onDelete }) { +function CalculatorEntries({ entries, parentId, getScopedParentId, activeEntryId, textContent, reorder, onEdit, onAddChild, onDelete }) { const children = getChildren(entries, parentId); - const [editingId, setEditingId] = useState(""); - useEffect(() => { - if (!editingId) return undefined; - - function closeOnOutsidePointer(event) { - if (event.target?.closest?.(`[data-calculator-edit-fields="${editingId}"]`)) return; - if (event.target?.closest?.("[data-calculator-edit-toggle]")) return; - setEditingId(""); - } - - document.addEventListener("pointerdown", closeOnOutsidePointer); - return () => document.removeEventListener("pointerdown", closeOnOutsidePointer); - }, [editingId]); - if (!children.length) return null; return (