import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import Icon from "./Icon"; import { gruppenOptionen, nachfahrenIds, pfadText } from "../groupGraph"; /** * Mehrfachauswahl der Obergruppen einer Gruppe. * * Bewusst KEIN Baum wie CategorySelect: eine Gruppe darf unter mehreren * Obergruppen hängen („Grillwurst" unter „Wurst" UND unter „Grillgut"). Ein * Baum müsste sie mehrfach anzeigen; eine flache, alphabetische Liste mit * Häkchen ist ehrlicher und deutlich weniger Code. Der volle Weg steht als * Titel an jeder Zeile, damit gleichnamige Äste unterscheidbar bleiben. * * Die eigene Gruppe und ihre Untergruppen sind gesperrt – sie würden einen Ring * erzeugen. Der Server lehnt das ohnehin ab; hier wird es vorher sichtbar. * * Das Menü hängt per Portal am , damit es der horizontal scrollende * Tabellenrahmen nicht abschneidet (wie bei CategorySelect). */ export default function GroupParentSelect({ value = [], onChange, groups = [], selfId = null, disabled = false, }) { const [open, setOpen] = useState(false); const [pos, setPos] = useState(null); const [filter, setFilter] = useState(""); const btnRef = useRef(null); const popRef = useRef(null); const gewaehlt = new Set(value || []); const gesperrt = selfId == null ? new Set() : new Set([selfId, ...nachfahrenIds(groups, selfId)]); const nameById = Object.fromEntries(groups.map((g) => [g.id, g.name])); // Als Baum, damit die Struktur beim Zuordnen sichtbar ist. Beim Suchen // flacht die Liste ab – eine Einrückung ohne ihre Eltern wäre irreführend. const sichtbar = filter ? groups .filter((g) => g.name.toLowerCase().includes(filter.toLowerCase())) .slice() .sort((a, b) => a.name.localeCompare(b.name, "de")) .map((g) => ({ id: g.id, key: `f${g.id}`, gruppe: g, tiefe: 0, label: g.name })) : gruppenOptionen(groups); const label = value?.length ? value.map((id) => nameById[id]).filter(Boolean).join(", ") : "– keine –"; function place() { const r = btnRef.current?.getBoundingClientRect(); if (r) setPos({ left: r.left, top: r.bottom + 4, width: r.width }); } useLayoutEffect(() => { if (open) place(); }, [open]); useEffect(() => { if (!open) return; function onDoc(e) { if (btnRef.current?.contains(e.target)) return; if (popRef.current?.contains(e.target)) return; setOpen(false); } function onScroll(e) { if (popRef.current && e.target instanceof Node && popRef.current.contains(e.target)) return; setOpen(false); } function onResize() { setOpen(false); } function onKey(e) { if (e.key === "Escape") setOpen(false); } document.addEventListener("mousedown", onDoc); window.addEventListener("scroll", onScroll, true); window.addEventListener("resize", onResize); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDoc); window.removeEventListener("scroll", onScroll, true); window.removeEventListener("resize", onResize); document.removeEventListener("keydown", onKey); }; }, [open]); // Bei Mehrfachauswahl bleibt das Menü offen – sonst müsste man es für jede // weitere Obergruppe neu aufklappen. function umschalten(id) { const neu = new Set(gewaehlt); if (neu.has(id)) neu.delete(id); else neu.add(id); onChange([...neu]); } return ( <> {open && pos && createPortal(
{groups.length > 8 && (
setFilter(e.target.value)} />
)} {sichtbar.map((o) => { const aus = gesperrt.has(o.id); const an = gewaehlt.has(o.id); return (
); })} {sichtbar.length === 0 && (
Keine Gruppe gefunden.
)}
, document.body, )} ); }