From 49ba0c4cb211cbfa0df1894453f1a49ab658f4d7 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 20 Aug 2026 16:16:24 +0200 Subject: [PATCH] Fix notepad cursor and calculator linked formulas --- docs/STORAGE_SCHEMA.md | 9 +- tests/helpers/data-validation.mjs | 4 +- tests/static-toolboxes.test.mjs | 2 +- tests/toolbox-modules.test.mjs | 5 +- website/public/data/site.json | 6 +- .../toolboxes/modules/CalculatorModule.jsx | 151 ++++++++++++++---- .../toolboxes/modules/NotepadModule.jsx | 63 +++++++- .../features/toolboxes/modules/textImport.js | 8 +- .../toolboxes/storage/modules/calculator.js | 8 +- .../styles/toolboxes/_calculator-results.scss | 12 ++ 10 files changed, 223 insertions(+), 45 deletions(-) diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 1222319..9ae6f21 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -661,13 +661,15 @@ Type : `calculator` { "id": "calc1", "label": "Lingots de fer", - "value": 100 + "value": 100, + "formula": "100" }, { "id": "calc2", "parentId": "calc1", "label": "Minerais de fer", - "value": 500 + "value": 500, + "formula": "base*5" } ] } @@ -679,6 +681,7 @@ Stockage compact : - `parentId` est omis si vide. - `label` peut être vide. - `value` est numérique. +- `formula` est omis si vide ; dans un calcul enfant, le mot `base` référence la valeur recalculée du parent. - Si un `parentId` ne pointe vers aucune entrée existante, il est réinitialisé à vide. ## Outil Tableau @@ -770,7 +773,7 @@ Formats : - `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`. - `links` : lignes `Titre: https://...` ou URL seule. - `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées. -- `calculator` : lignes `Libellé: valeur`, indentation de deux espaces pour les enfants, meta `@scrollResults: true`. +- `calculator` : lignes `Libellé: valeur | formula=base*5`, indentation de deux espaces pour les enfants, meta `@scrollResults: true`. Le suffixe `formula=...` est optionnel et les anciens exports `Libellé: valeur` restent acceptés. - `table` : TSV pur accepté ; l'export peut ajouter `@size`, `@columns`, `@rows` puis `@tsv` pour préserver dimensions et intitulés. - `combos` : sections `# Catégorie`, lignes `Nom | device=... | kind:value > kind:value+kind:value`, avec suffixes `[hold]` ou `[2s]`. - `taskPlanner` : sections `# Catégorie`, lignes indentées `- daily Titre`, `- weekly Titre` ou `- unique Titre`, metas `@description`, `@dailyResetTime`, `@weeklyResetDay`, `@prerequisite`. diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 5074726..cb0d658 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -263,7 +263,9 @@ export function validateSiteContent(site) { "toolboxes.modules.calculator.copiedTitle", "toolboxes.modules.calculator.scrollableTitle", "toolboxes.modules.calculator.emptyResults", - "toolboxes.modules.calculator.readonlyValueTitle", + "toolboxes.modules.calculator.formulaTitle", + "toolboxes.modules.calculator.childFormulaHelp", + "toolboxes.modules.calculator.rootFormulaHelp", "toolboxes.modules.calculator.useEntryTitle", "toolboxes.modules.calculator.renameTitle", "toolboxes.modules.calculator.deleteTitle", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 96aab94..d9f384e 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -251,7 +251,7 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /resetCalculator/); assert.match(calculatorModule, /returnToRoot/); assert.match(calculatorModule, /setExpression\(activeParent \? formatResult\(activeParent\.value\) : ""\)/); - assert.match(calculatorModule, /Quantité non modifiable/); + assert.match(calculatorModule, /EditableCalculatorFormula/); assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/); assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /--calculator-scroll-height/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index fd89646..7d5cc48 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -107,14 +107,15 @@ test("text exchange imports table TSV and calculator hierarchy", () => { const calculator = normalizeCalculatorData({ scrollResults: true, entries: [ - { id: "a", label: "Ore", value: 100 }, - { id: "b", parentId: "a", label: "Shard", value: 500 } + { id: "a", label: "Ore", value: 100, formula: "100" }, + { id: "b", parentId: "a", label: "Shard", value: 500, formula: "base*5" } ] }); const calculatorImport = importModuleText("calculator", exportModuleText("calculator", calculator), context); assert.equal(calculatorImport.ok, true); assert.equal(calculatorImport.data.scrollResults, true); assert.equal(calculatorImport.data.entries[1].parentId, calculatorImport.data.entries[0].id); + assert.equal(calculatorImport.data.entries[1].formula, "base*5"); }); test("text exchange round trips combos, tasks, equipment, images and timers", () => { diff --git a/website/public/data/site.json b/website/public/data/site.json index 13b9bc6..ef70b05 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -1763,7 +1763,7 @@ "deleteComboTitle": "Supprimer le combo" }, "calculator": { - "importPlaceholder": "@scrollResults: true\nLingots de fer: 100\n Minerais de fer: 500", + "importPlaceholder": "@scrollResults: true\nLingots de fer: 100 | formula=100\n Minerais de fer: 500 | formula=base*5", "importOpenButton": "Importer du texte", "exportOpenButton": "Exporter en format texte", "importModalTitle": "Importer des résultats", @@ -1785,7 +1785,9 @@ "copiedTitle": "Copié", "scrollableTitle": "Liste scrollable", "emptyResults": "Aucun résultat enregistré.", - "readonlyValueTitle": "Quantité non modifiable", + "formulaTitle": "Modifier la formule", + "childFormulaHelp": "Utilisez base pour référencer le parent.", + "rootFormulaHelp": "Valeur ou formule racine.", "useEntryTitle": "Utiliser comme base", "renameTitle": "Renommer", "deleteTitle": "Supprimer" diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx index 88b8563..edbb970 100644 --- a/website/src/features/toolboxes/modules/CalculatorModule.jsx +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -1,18 +1,21 @@ // Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte. -import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; const DISPLAY_NUMBER_FORMATTER = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 6 }); +const BASE_TOKEN = "base"; -function calculateExpression(expression) { +function calculateExpression(expression, baseValue = null) { const normalized = String(expression || "").replaceAll(",", ".").trim(); - if (!normalized || !/^[\d\s+\-*/().]+$/.test(normalized)) return null; + if (!normalized) return null; + const withBase = normalized.replace(/\bbase\b/gi, () => Number.isFinite(baseValue) ? `(${formatResult(baseValue)})` : "NaN"); + if (!/^[\d\s+\-*/().]+$/.test(withBase)) return null; try { - const value = Function(`"use strict"; return (${normalized})`)(); + const value = Function(`"use strict"; return (${withBase})`)(); return Number.isFinite(value) ? value : null; } catch { return null; @@ -56,6 +59,48 @@ function getEntriesInTreeOrder(entries, parentId = "") { return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]); } +function getEntryFormula(entry) { + return String(entry?.formula || "").trim(); +} + +function getEditableFormula(entry) { + return getEntryFormula(entry) || formatResult(entry.value); +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function createStoredFormula(expression, parentValue) { + const normalized = String(expression || "").replaceAll(",", ".").trim(); + if (!Number.isFinite(parentValue)) return normalized; + const parentText = formatResult(parentValue); + if (/\bbase\b/i.test(normalized)) return normalized; + const withBase = normalized.replace(new RegExp(`(^|[^\\d.])${escapeRegExp(parentText)}(?=$|[^\\d.])`, "g"), `$1${BASE_TOKEN}`); + if (withBase !== normalized) return withBase; + if (/^[+\-*/]/.test(normalized)) return `${BASE_TOKEN}${normalized}`; + return normalized; +} + +function evaluateCalculatorEntries(entries) { + const byParent = new Map(); + entries.forEach((entry) => { + const parentId = entry.parentId || ""; + byParent.set(parentId, [...(byParent.get(parentId) || []), entry]); + }); + + function visit(parentId = "", parentValue = null) { + return (byParent.get(parentId) || []).flatMap((entry) => { + const formula = getEntryFormula(entry); + const calculatedValue = formula ? calculateExpression(formula, parentValue) : entry.value; + const nextEntry = { ...entry, value: calculatedValue ?? entry.value }; + return [nextEntry, ...visit(entry.id, nextEntry.value)]; + }); + } + + return visit(); +} + function FormattedCalculatorNumber({ value }) { if (!Number.isFinite(value)) return "-"; let integerGroupIndex = 0; @@ -73,6 +118,8 @@ function FormattedCalculatorNumber({ value }) { export function CalculatorModule({ toolboxId, moduleId, context, editing }) { const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); + const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]); + const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]); const textContent = context.moduleText?.calculator || {}; const [expression, setExpression] = useState(""); const [label, setLabel] = useState(""); @@ -80,16 +127,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { const [calculatorHeight, setCalculatorHeight] = useState(0); const [copied, setCopied] = useState(false); const calculatorCardRef = useRef(null); - const result = useMemo(() => calculateExpression(expression), [expression]); - const activeParent = data.entries.find((entry) => entry.id === activeParentId); + const activeParent = calculatedEntries.find((entry) => entry.id === activeParentId); + const result = useMemo(() => calculateExpression(expression, activeParent?.value), [activeParent?.value, expression]); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", - items: data.entries, + items: calculatedEntries, getItemId: (entry) => entry.id, getParentId: (entry) => scopeParentId(entry.parentId || ""), onItemMove: (operation) => { - save(moveItem(data.entries, operation.sourceId, operation.targetId, operation.placement)); + save(moveItem(calculatedEntries, operation.sourceId, operation.targetId, operation.placement)); }, hierarchy: { enabled: true, stickyParents: true } }); @@ -109,7 +156,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { }, []); function save(entries) { - context.setModuleData(toolboxId, moduleId, { ...data, entries }); + context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) }); } function setScrollResults(scrollResults) { @@ -127,7 +174,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { id: context.uid("calc"), parentId: activeParentId, label: cleanLabel, - value: result + value: result, + formula: createStoredFormula(expression, activeParent?.value) } ]); setExpression(activeParent ? formatResult(activeParent.value) : ""); @@ -140,17 +188,28 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { } function deleteEntry(entryId) { - const deletedIds = new Set([entryId, ...getDescendantIds(data.entries, entryId)]); + const deletedIds = new Set([entryId, ...getDescendantIds(calculatedEntries, entryId)]); if (deletedIds.has(activeParentId)) { setActiveParentId(""); setExpression(""); } - save(data.entries.filter((entry) => !deletedIds.has(entry.id))); + save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); } function renameEntry(entryId, label) { const cleanLabel = label.trim(); - save(data.entries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry)); + 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() { @@ -165,7 +224,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { } async function copyChecklistImport() { - const text = getEntriesInTreeOrder(data.entries) + const text = getEntriesInTreeOrder(calculatedEntries) .map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`) .join("\n"); if (!text || !await context.copyText(text)) return; @@ -178,7 +237,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
{editing && (
- context.setModuleData(toolboxId, moduleId, nextData, "calculator")} /> + context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
)}
@@ -229,8 +288,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
- {data.entries.length ? ( - + {calculatedEntries.length ? ( + ) : (

{textContent.emptyResults || "Aucun résultat enregistré."}

)} @@ -240,9 +299,21 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) { ); } -function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, reorder, onUse, onRename, onDelete }) { +function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, editing, reorder, onUse, onRename, onUpdateFormula, 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; + setEditingId(""); + } + + document.addEventListener("pointerdown", closeOnOutsidePointer); + return () => document.removeEventListener("pointerdown", closeOnOutsidePointer); + }, [editingId]); + if (!children.length) return null; return ( @@ -251,6 +322,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI const className = [ entry.id === activeParentId ? "active" : "", entry.id === editingId ? "is-editing" : "", + "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" : "" @@ -269,14 +341,10 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI {entry.id === editingId ? ( - <> - - - - - - setEditingId("")} /> - + + + + ) : (
- + ); })} @@ -302,12 +370,12 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI ); } -function EditableCalculatorLabel({ entry, textContent, onRename, onDone }) { +function EditableCalculatorLabel({ entry, textContent, onRename, onDone, autoFocus = true }) { const inlineEdit = useInlineEdit({ value: entry.label, onCommit: (label) => onRename(entry.id, label), onDone, - autoFocus: true, + autoFocus, focusKey: entry.id }); @@ -320,3 +388,28 @@ function EditableCalculatorLabel({ entry, textContent, onRename, onDone }) { /> ); } + +function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormula, autoFocus = false }) { + const parent = entries.find((candidate) => candidate.id === entry.parentId); + const parentValue = parent?.value; + const inlineEdit = useInlineEdit({ + value: getEditableFormula(entry), + onCommit: (formula) => { + const storedFormula = createStoredFormula(formula, parentValue); + if (calculateExpression(storedFormula, parentValue) != null) onUpdateFormula(entry.id, storedFormula); + }, + autoFocus, + focusKey: `${entry.id}:formula`, + transform: (draft) => String(draft || "").replaceAll(",", ".").trim() + }); + + return ( + + ); +} diff --git a/website/src/features/toolboxes/modules/NotepadModule.jsx b/website/src/features/toolboxes/modules/NotepadModule.jsx index 8cba4ed..fa9040c 100644 --- a/website/src/features/toolboxes/modules/NotepadModule.jsx +++ b/website/src/features/toolboxes/modules/NotepadModule.jsx @@ -1,5 +1,5 @@ // Rôle : fournit l'outil bloc-notes riche avec annotations dessinées. -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx"; import { DrawingOverlay } from "./DrawingOverlay.jsx"; @@ -13,6 +13,43 @@ function getPlainText(element) { return (element?.innerText || "").replace(/\n{3,}/g, "\n\n").trim(); } +function getSelectionTextOffset(container) { + const selection = document.getSelection(); + if (!container || !selection?.rangeCount || !container.contains(selection.anchorNode)) return null; + const range = selection.getRangeAt(0); + const prefix = range.cloneRange(); + prefix.selectNodeContents(container); + prefix.setEnd(range.startContainer, range.startOffset); + return prefix.toString().length; +} + +function restoreSelectionTextOffset(container, offset) { + if (!container || offset == null) return; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); + let remaining = offset; + let node = walker.nextNode(); + while (node) { + const length = node.textContent.length; + if (remaining <= length) { + const range = document.createRange(); + range.setStart(node, remaining); + range.collapse(true); + const selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + remaining -= length; + node = walker.nextNode(); + } + const range = document.createRange(); + range.selectNodeContents(container); + range.collapse(false); + const selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +} + function isHtmlEmpty(html, text) { return !text && !String(html || "").replace(//gi, "").replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim(); } @@ -114,6 +151,9 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false }) const textContent = context.moduleText?.notepad || {}; const editorRef = useRef(null); const surfaceRef = useRef(null); + const selectionOffsetRef = useRef(null); + const localSaveUpdatedAtRef = useRef(""); + const moduleKeyRef = useRef(`${toolboxId}:${moduleId}`); const [html, setHtml] = useState(normalizedData.html); const [text, setText] = useState(normalizedData.text); const [updatedAt, setUpdatedAt] = useState(normalizedData.updatedAt); @@ -137,6 +177,19 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false }) const [temporaryStrokes, setTemporaryStrokes] = useState([]); useEffect(() => { + const moduleKey = `${toolboxId}:${moduleId}`; + if (moduleKeyRef.current !== moduleKey) { + moduleKeyRef.current = moduleKey; + localSaveUpdatedAtRef.current = ""; + } + if (normalizedData.updatedAt && normalizedData.updatedAt === localSaveUpdatedAtRef.current) return; + const editor = editorRef.current; + if (document.activeElement === editor && getPlainText(editor) === normalizedData.text) { + setText(normalizedData.text); + setUpdatedAt(normalizedData.updatedAt); + setDrawingMode(normalizedData.drawingMode); + return; + } setHtml(normalizedData.html); setText(normalizedData.text); setUpdatedAt(normalizedData.updatedAt); @@ -145,10 +198,12 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false }) setEraseActive(false); }, [toolboxId, moduleId, normalizedData.html, normalizedData.text, normalizedData.updatedAt, normalizedData.drawingMode]); - useEffect(() => { + useLayoutEffect(() => { if (!editorRef.current || editorRef.current.innerHTML === html) return; + if (document.activeElement === editorRef.current && getPlainText(editorRef.current) === text) return; editorRef.current.innerHTML = html; - }, [html]); + if (document.activeElement === editorRef.current) restoreSelectionTextOffset(editorRef.current, selectionOffsetRef.current); + }, [html, text]); useEffect(() => { function updateActiveFormats() { @@ -199,6 +254,7 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false }) function saveContent(nextHtml, nextText) { const nextUpdatedAt = new Date().toISOString(); + localSaveUpdatedAtRef.current = nextUpdatedAt; setHtml(nextHtml); setText(nextText); setUpdatedAt(nextUpdatedAt); @@ -241,6 +297,7 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false }) } function handleEditorInput() { + selectionOffsetRef.current = getSelectionTextOffset(editorRef.current); const nextHtml = editorRef.current?.innerHTML || ""; const nextText = getPlainText(editorRef.current); saveContent(nextHtml, nextText); diff --git a/website/src/features/toolboxes/modules/textImport.js b/website/src/features/toolboxes/modules/textImport.js index d183c79..0ae548e 100644 --- a/website/src/features/toolboxes/modules/textImport.js +++ b/website/src/features/toolboxes/modules/textImport.js @@ -340,7 +340,8 @@ function exportCalculatorText(data) { if (data.scrollResults) lines.push("@scrollResults: true"); function visit(parentId = "", depth = 0) { (data.entries || []).filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => { - lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}`); + const formula = String(entry.formula || "").trim(); + lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}`); visit(entry.id, depth + 1); }); } @@ -363,9 +364,10 @@ function importCalculatorText(text, context) { const separatorIndex = line.indexOf(":"); if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`); const label = line.slice(0, separatorIndex).trim(); - const value = parseNumber(line.slice(separatorIndex + 1), Number.NaN); + const [rawValue, rawFormula = ""] = line.slice(separatorIndex + 1).split(/\s+\|\s+formula=/); + const value = parseNumber(rawValue, Number.NaN); if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`); - const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value }; + const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula: rawFormula.trim() }; entries.push(entry); parentStack[depth] = entry.id; parentStack.length = depth + 1; diff --git a/website/src/features/toolboxes/storage/modules/calculator.js b/website/src/features/toolboxes/storage/modules/calculator.js index eebfeac..7143fb8 100644 --- a/website/src/features/toolboxes/storage/modules/calculator.js +++ b/website/src/features/toolboxes/storage/modules/calculator.js @@ -6,13 +6,18 @@ function normalizeCalculatorValue(value) { return Number.isFinite(parsed) ? parsed : 0; } +function normalizeCalculatorFormula(value) { + return String(value || "").replaceAll(",", ".").trim(); +} + export function normalizeCalculatorData(data) { const entries = (data?.entries || []) .map((entry) => ({ id: entry?.id || uid("calc"), parentId: String(entry?.parentId || ""), label: String(entry?.label || "").trim(), - value: normalizeCalculatorValue(entry?.value) + value: normalizeCalculatorValue(entry?.value), + formula: normalizeCalculatorFormula(entry?.formula) })); const entryIds = new Set(entries.map((entry) => entry.id)); return { @@ -26,6 +31,7 @@ export function compactCalculatorDataForStorage(value) { const entries = normalized.entries.map((entry) => { const compact = { id: entry.id, label: entry.label, value: entry.value }; if (entry.parentId) compact.parentId = entry.parentId; + if (entry.formula) compact.formula = entry.formula; return compact; }); if (!entries.length && !normalized.scrollResults) return null; diff --git a/website/src/styles/toolboxes/_calculator-results.scss b/website/src/styles/toolboxes/_calculator-results.scss index 1b29cba..e580461 100644 --- a/website/src/styles/toolboxes/_calculator-results.scss +++ b/website/src/styles/toolboxes/_calculator-results.scss @@ -30,6 +30,18 @@ grid-template-columns: 30px auto minmax(0, 1fr) 34px 34px; } +.tool-split-entry-list li.is-editing.can-edit-formula > .calculator-entry.has-drag-handle { + grid-template-columns: 30px minmax(92px, 0.9fr) minmax(80px, 1fr) 34px 34px; +} + +.calculator-entry-formula { + font-variant-numeric: tabular-nums; +} + +.calculator-entry-edit-fields { + display: contents; +} + .calculator-drag-handle { width: 30px; min-width: 30px;