Web: DataTable - Spalten ein-/ausblenden (Spalten-Waehler)

Neuer 'Spalten'-Button in der Tabellen-Toolbar oeffnet ein Popover mit einer Checkbox je Spalte. Abgewaehlte Spalten werden ausgeblendet (Kopf, Filterzeile, Zellen), wieder anhaken zeigt sie an alter Position. Die Auswahl merkt sich der Browser (localStorage, je Tabelle), mindestens eine Spalte bleibt sichtbar, 'Spalten zuruecksetzen' zeigt wieder alle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 17:58:01 +02:00
parent 08ee1f20f5
commit e2d2347524

View File

@@ -44,8 +44,14 @@ export default function DataTable({
const [filterShown, setFilterShown] = useState(true); const [filterShown, setFilterShown] = useState(true);
const [openFilter, setOpenFilter] = useState(null); const [openFilter, setOpenFilter] = useState(null);
const [collapsed, setCollapsed] = useState(() => new Set()); const [collapsed, setCollapsed] = useState(() => new Set());
const [hidden, setHidden] = useState(
() => new Set((loadPersisted(id).hidden || []).filter((k) => colByKey[k])),
);
const [chooserOpen, setChooserOpen] = useState(false);
useEffect(() => { savePersisted(id, { order, widths }); }, [id, order, widths]); useEffect(() => {
savePersisted(id, { order, widths, hidden: [...hidden] });
}, [id, order, widths, hidden]);
// Containerbreite messen, damit die grow-Spalte den Rest füllt. // Containerbreite messen, damit die grow-Spalte den Rest füllt.
const wrapRef = useRef(null); const wrapRef = useRef(null);
@@ -59,7 +65,39 @@ export default function DataTable({
return () => ro.disconnect(); return () => ro.disconnect();
}, []); }, []);
const orderedCols = order.map((k) => colByKey[k]).filter(Boolean); const allOrdered = order.map((k) => colByKey[k]).filter(Boolean);
const orderedCols = allOrdered.filter((c) => !hidden.has(c.key));
// Spalten-Wähler-Popover.
const chooserBtn = useRef(null);
const chooserPop = useRef(null);
const [chooserPos, setChooserPos] = useState(null);
useLayoutEffect(() => {
if (chooserOpen && chooserBtn.current) {
const r = chooserBtn.current.getBoundingClientRect();
setChooserPos({ left: Math.min(r.left, window.innerWidth - 240), top: r.bottom + 4 });
}
}, [chooserOpen]);
useEffect(() => {
if (!chooserOpen) return undefined;
function onDoc(e) {
if (chooserBtn.current?.contains(e.target) || chooserPop.current?.contains(e.target)) return;
setChooserOpen(false);
}
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [chooserOpen]);
function toggleHidden(key) {
setHidden((s) => {
const n = new Set(s);
if (n.has(key)) { n.delete(key); return n; }
// Mindestens eine Spalte sichtbar lassen.
if (allOrdered.filter((c) => !n.has(c.key) && c.key !== key).length === 0) return s;
n.add(key);
return n;
});
}
const isGrow = (c) => c.grow && widths[c.key] == null; const isGrow = (c) => c.grow && widths[c.key] == null;
const fixedWidth = (c) => widths[c.key] ?? c.width ?? DEFAULT_WIDTH; const fixedWidth = (c) => widths[c.key] ?? c.width ?? DEFAULT_WIDTH;
@@ -224,6 +262,7 @@ export default function DataTable({
function resetColumns() { function resetColumns() {
setOrder(columns.map((c) => c.key)); setOrder(columns.map((c) => c.key));
setWidths({}); setWidths({});
setHidden(new Set());
savePersisted(id, {}); savePersisted(id, {});
} }
@@ -235,9 +274,26 @@ export default function DataTable({
<Icon name="search" size={14} />Filter <Icon name="search" size={14} />Filter
</button> </button>
<button type="button" className="btn sm ghost" onClick={() => setFilters({})}>Filter löschen</button> <button type="button" className="btn sm ghost" onClick={() => setFilters({})}>Filter löschen</button>
<button type="button" ref={chooserBtn} className={`btn sm ${chooserOpen ? "" : "ghost"}`}
onClick={() => setChooserOpen((o) => !o)}>
<Icon name="settings" size={14} />Spalten
</button>
<button type="button" className="btn sm ghost" onClick={resetColumns}>Spalten zurücksetzen</button> <button type="button" className="btn sm ghost" onClick={resetColumns}>Spalten zurücksetzen</button>
{toolbarExtra} {toolbarExtra}
</div> </div>
{chooserOpen && chooserPos && createPortal(
<div ref={chooserPop} className="dt-pop" style={{ left: chooserPos.left, top: chooserPos.top }}>
<div className="dt-pop-list">
{allOrdered.map((c) => (
<label key={c.key} className="dt-pop-opt">
<input type="checkbox" checked={!hidden.has(c.key)} onChange={() => toggleHidden(c.key)} />
<span>{c.header || c.key}</span>
</label>
))}
</div>
</div>,
document.body,
)}
<div className="table-wrap" ref={wrapRef}> <div className="table-wrap" ref={wrapRef}>
<table className="table datatable" style={{ width: totalWidth ? `${totalWidth}px` : "100%" }}> <table className="table datatable" style={{ width: totalWidth ? `${totalWidth}px` : "100%" }}>
<colgroup> <colgroup>