105 lines
3.7 KiB
JavaScript
105 lines
3.7 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) {
|
|
const cleaned = String(text).trim().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);
|
|
return num.toLocaleString("de-DE", { maximumFractionDigits: 3 });
|
|
}
|