// Kleine DOM-Hilfen. Ersetzt kein Framework, spart nur Wiederholung. "use strict"; /** el("button.primary", { onclick: fn }, "Text") * Setzt Text ausschließlich über textContent - dadurch kann kein * Benutzereingabewert als HTML interpretiert werden. */ export function el(spec, props = {}, ...children) { const [tag, ...classes] = String(spec).split("."); const node = document.createElement(tag || "div"); if (classes.length) node.className = classes.join(" "); for (const [key, value] of Object.entries(props)) { if (value === null || value === undefined || value === false) continue; if (key.startsWith("on") && typeof value === "function") { node.addEventListener(key.slice(2).toLowerCase(), value); } else if (key === "dataset") { Object.assign(node.dataset, value); } else if (key in node && key !== "list") { node[key] = value; } else { node.setAttribute(key, value === true ? "" : value); } } append(node, children); return node; } function append(node, children) { for (const child of children.flat(Infinity)) { if (child === null || child === undefined || child === false) continue; node.append(child instanceof Node ? child : document.createTextNode(String(child))); } } export function clear(node) { node.replaceChildren(); return node; } /** Ersetzt den Inhalt eines Knotens. * * Nicht clear(root).append(...) verwenden: Node.append() macht aus einem * null die Zeichenkette "null" und zeigt sie an. Genau dafür filtert * el() seine Kinder - mount() zieht denselben Schutz auf die Wurzel. */ export function mount(node, ...children) { node.replaceChildren(); for (const child of children.flat(Infinity)) { if (child === null || child === undefined || child === false) continue; node.append(child instanceof Node ? child : document.createTextNode(String(child))); } return node; } /** Preis in Cent -> "1,29 €". Rechnen immer in Cent, formatieren erst hier. */ export function euro(cents) { return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format((cents || 0) / 100); } /** "1,29" oder "1.29" -> 129. Gibt null zurück, wenn nichts Sinnvolles drinsteht. */ export function parseCents(text) { const cleaned = String(text).trim().replace(",", "."); if (!cleaned) return null; const value = Number(cleaned); if (!Number.isFinite(value) || value < 0) return null; return Math.round(value * 100); } export function parseQuantity(text) { // Leerzeichen und Tausenderpunkte entfernen, Komma als Dezimaltrenner. // Die Punkte fallen weg, weil eine Eingabe wie "1.500" im Deutschen // eintausendfünfhundert meint - und weil formatQuantity keine // Tausendertrennung mehr erzeugt, bleibt kein anderer Fall übrig. const cleaned = String(text) .trim() .replace(/[\s\u00a0.]/g, "") .replace(",", "."); if (!cleaned) return null; const value = Number(cleaned); return Number.isFinite(value) && value >= 0 ? value : null; } /** Stückzahl: ganze Zahl ab 1. Leer oder unsinnig ergibt 1 - ein Eintrag * ohne Stückzahl wäre unvollständig. */ export function parseCount(text) { const value = Number(String(text).trim().replace(",", ".")); if (!Number.isFinite(value) || value < 1) return 1; return Math.min(999, Math.round(value)); } /** Nur das Gebinde, ohne Stückzahl. * * Wird in der Listenzeile gebraucht, weil die Stückzahl dort als * eigenes Feld links neben dem Namen steht - beim Einkaufen soll sie * ins Auge springen und nicht im Kleingedruckten stehen. */ export function formatPack(packSize, packUnit) { if (!packSize) return ""; return `${formatQuantity(packSize)}${packUnit ? " " + packUnit : ""}`; } /** Menge aus der API kommt als Zeichenkette ("2.000"), damit unterwegs * keine Nachkommastellen verloren gehen. Für die Anzeige aufräumen. */ export function formatQuantity(value) { if (value === null || value === undefined) return ""; const num = Number(value); if (!Number.isFinite(num)) return String(value); // OHNE Tausendertrennung. Der Wert landet auch in Eingabefeldern, und // parseQuantity liest den deutschen Tausenderpunkt als Dezimalpunkt: // Aus einer 1000-Gramm-Packung wurde beim Bearbeiten eine mit 1 Gramm. // Bei Mengen bringt die Trennung ohnehin wenig - "1000 g" liest sich // gut genug. return num.toLocaleString("de-DE", { maximumFractionDigits: 3, useGrouping: false, }); }