Files
einkaufsapp/web/html/js/dom.js

120 lines
4.4 KiB
JavaScript

// 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,
});
}