Add internal mix tag input for calculator and goal tree formulas
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-24 13:24:44 +02:00
parent 5c8148c905
commit ae503c7260
13 changed files with 929 additions and 23 deletions

View file

@ -1,6 +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 { 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";
@ -183,8 +184,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
}
function useEntry(entry) {
setExpression(formatResult(entry.value));
setActiveParentId(entry.id);
setExpression(BASE_TOKEN);
}
function deleteEntry(entryId) {
@ -242,10 +243,48 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
)}
<form className="calculator-form" onSubmit={saveResult}>
<div className="calculator-card" ref={calculatorCardRef}>
<label>
<div className="calculator-field">
<span>{textContent.expressionLabel || "Calcul"}</span>
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
</label>
{activeParent ? (
<MixTagsInput
aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input"
onChange={setExpression}
onKeyDown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
event.currentTarget.closest("form")?.requestSubmit();
}}
formulaSpacing
insertToken={{
value: "base",
label: textContent.baseTokenLabel || "Valeur du parent",
icon: "parent",
title: textContent.insertBaseTitle || "Ajouter la valeur du parent"
}}
placeholder={textContent.formulaPlaceholder || "base * 3"}
tokens={[{
value: "base",
label: textContent.baseTokenLabel || "Valeur du parent"
}]}
value={expression}
/>
) : (
<input
aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input"
onChange={(event) => setExpression(event.target.value)}
onKeyDown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
event.currentTarget.closest("form")?.requestSubmit();
}}
inputMode="decimal"
placeholder={textContent.expressionPlaceholder || "10*10"}
value={expression}
/>
)}
</div>
<div className={`calculator-result ${result == null ? "" : getResultLengthClass(result)}`} aria-live="polite">
<span>{textContent.resultLabel || "Résultat"}</span>
<strong aria-label={result == null ? "-" : formatDisplayResult(result)}>
@ -307,6 +346,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
function closeOnOutsidePointer(event) {
if (event.target?.closest?.(`[data-calculator-edit-fields="${editingId}"]`)) return;
if (event.target?.closest?.("[data-calculator-edit-toggle]")) return;
setEditingId("");
}
@ -322,7 +362,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
const className = [
entry.id === activeParentId ? "active" : "",
entry.id === editingId ? "is-editing" : "",
"can-edit-formula",
entry.parentId ? "can-edit-formula" : "",
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
@ -355,7 +395,15 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
</span>
</button>
)}
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`} title={textContent.renameTitle || "Renommer"}>
<button
className={entry.id === editingId ? "active" : ""}
type="button"
onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)}
aria-pressed={entry.id === editingId}
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`}
data-calculator-edit-toggle
title={textContent.renameTitle || "Renommer"}
>
<Icon name="edit" />
</button>
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
@ -392,6 +440,7 @@ function EditableCalculatorLabel({ entry, textContent, onRename, onDone, autoFoc
function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormula, autoFocus = false }) {
const parent = entries.find((candidate) => candidate.id === entry.parentId);
const parentValue = parent?.value;
const hasParent = Boolean(entry.parentId);
const inlineEdit = useInlineEdit({
value: getEditableFormula(entry),
onCommit: (formula) => {
@ -403,13 +452,49 @@ function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormul
transform: (draft) => String(draft || "").replaceAll(",", ".").trim()
});
if (!hasParent) {
return (
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label calculator-entry-formula",
"aria-label": `${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`,
title: textContent.rootFormulaHelp || "Valeur ou formule racine."
})}
/>
);
}
return (
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label calculator-entry-formula",
"aria-label": `${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`,
title: entry.parentId ? textContent.childFormulaHelp || "Utilisez base pour référencer le parent." : textContent.rootFormulaHelp || "Valeur ou formule racine."
})}
<MixTagsInput
aria-label={`${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`}
className="tool-split-entry-label calculator-entry-formula"
onBlur={() => inlineEdit.commit()}
onChange={inlineEdit.setDraft}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
event.currentTarget.blur();
}
if (event.key === "Escape") {
event.preventDefault();
inlineEdit.cancel(event);
}
}}
formulaSpacing
insertToken={{
value: "base",
label: textContent.baseTokenLabel || "Valeur du parent",
icon: "parent",
title: textContent.insertBaseTitle || "Ajouter la valeur du parent"
}}
placeholder={textContent.formulaPlaceholder || "base * 3"}
size="small"
title={entry.parentId ? textContent.childFormulaHelp || "Utilisez base pour référencer le parent." : textContent.rootFormulaHelp || "Valeur ou formule racine."}
tokens={[{
value: "base",
label: textContent.baseTokenLabel || "Valeur du parent"
}]}
value={inlineEdit.draft}
/>
);
}