@konce-pt/react 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/accordion/accordion.d.ts +45 -0
- package/dist/accordion/accordion.js +69 -0
- package/dist/alert/alert.d.ts +25 -0
- package/dist/alert/alert.js +31 -0
- package/dist/app-shell/app-shell.d.ts +94 -0
- package/dist/app-shell/app-shell.js +105 -0
- package/dist/auth/auth-types.d.ts +44 -0
- package/dist/auth/auth-types.js +2 -0
- package/dist/auth/auth.d.ts +76 -0
- package/dist/auth/auth.js +184 -0
- package/dist/autocomplete/autocomplete.d.ts +18 -0
- package/dist/autocomplete/autocomplete.js +72 -0
- package/dist/avatar/avatar.d.ts +33 -0
- package/dist/avatar/avatar.js +30 -0
- package/dist/avatar-group/avatar-group.d.ts +21 -0
- package/dist/avatar-group/avatar-group.js +15 -0
- package/dist/badge/badge.d.ts +25 -0
- package/dist/badge/badge.js +14 -0
- package/dist/bottom-sheet/bottom-sheet.d.ts +21 -0
- package/dist/bottom-sheet/bottom-sheet.js +22 -0
- package/dist/breadcrumb/breadcrumb.d.ts +19 -0
- package/dist/breadcrumb/breadcrumb.js +20 -0
- package/dist/button/button.d.ts +27 -0
- package/dist/button/button.js +49 -0
- package/dist/button-group/button-group.d.ts +25 -0
- package/dist/button-group/button-group.js +30 -0
- package/dist/card/card.d.ts +66 -0
- package/dist/card/card.js +96 -0
- package/dist/carousel/carousel.d.ts +20 -0
- package/dist/carousel/carousel.js +24 -0
- package/dist/charts/chart-legend.d.ts +23 -0
- package/dist/charts/chart-legend.js +9 -0
- package/dist/charts/chart-tooltip.d.ts +21 -0
- package/dist/charts/chart-tooltip.js +13 -0
- package/dist/charts/chart.d.ts +18 -0
- package/dist/charts/chart.js +231 -0
- package/dist/charts-entry/public-api.d.ts +4 -0
- package/dist/charts-entry/public-api.js +7 -0
- package/dist/checkbox/checkbox.d.ts +33 -0
- package/dist/checkbox/checkbox.js +29 -0
- package/dist/chip/chip.d.ts +22 -0
- package/dist/chip/chip.js +16 -0
- package/dist/chips-input/chips-input.d.ts +25 -0
- package/dist/chips-input/chips-input.js +54 -0
- package/dist/clock/clock.d.ts +17 -0
- package/dist/clock/clock.js +57 -0
- package/dist/color-picker/color-picker.d.ts +23 -0
- package/dist/color-picker/color-picker.js +28 -0
- package/dist/confirm/confirm-container.d.ts +13 -0
- package/dist/confirm/confirm-container.js +26 -0
- package/dist/confirm/confirm-store.d.ts +29 -0
- package/dist/confirm/confirm-store.js +52 -0
- package/dist/context-menu/context-menu.d.ts +21 -0
- package/dist/context-menu/context-menu.js +36 -0
- package/dist/data-table/data-table-types.d.ts +67 -0
- package/dist/data-table/data-table-types.js +1 -0
- package/dist/data-table/data-table.d.ts +62 -0
- package/dist/data-table/data-table.js +453 -0
- package/dist/data-table/export-csv.d.ts +7 -0
- package/dist/data-table/export-csv.js +19 -0
- package/dist/data-view/data-view.d.ts +21 -0
- package/dist/data-view/data-view.js +25 -0
- package/dist/date-range/date-range.d.ts +30 -0
- package/dist/date-range/date-range.js +155 -0
- package/dist/datepicker/datepicker.d.ts +39 -0
- package/dist/datepicker/datepicker.js +202 -0
- package/dist/dialog/dialog.d.ts +35 -0
- package/dist/dialog/dialog.js +59 -0
- package/dist/divider/divider.d.ts +16 -0
- package/dist/divider/divider.js +13 -0
- package/dist/drawer/drawer.d.ts +25 -0
- package/dist/drawer/drawer.js +25 -0
- package/dist/empty/empty.d.ts +18 -0
- package/dist/empty/empty.js +14 -0
- package/dist/fab/fab.d.ts +22 -0
- package/dist/fab/fab.js +17 -0
- package/dist/fieldset/fieldset.d.ts +22 -0
- package/dist/fieldset/fieldset.js +24 -0
- package/dist/file-upload/file-upload.d.ts +22 -0
- package/dist/file-upload/file-upload.js +50 -0
- package/dist/form-field/form-field-context.d.ts +23 -0
- package/dist/form-field/form-field-context.js +17 -0
- package/dist/form-field/form-field.d.ts +39 -0
- package/dist/form-field/form-field.js +41 -0
- package/dist/galleria/galleria.d.ts +22 -0
- package/dist/galleria/galleria.js +21 -0
- package/dist/grid/col.d.ts +39 -0
- package/dist/grid/col.js +32 -0
- package/dist/grid/flex.d.ts +45 -0
- package/dist/grid/flex.js +35 -0
- package/dist/grid/grid.d.ts +47 -0
- package/dist/grid/grid.js +38 -0
- package/dist/grid/layout.d.ts +18 -0
- package/dist/grid/layout.js +32 -0
- package/dist/grid-entry/public-api.d.ts +6 -0
- package/dist/grid-entry/public-api.js +8 -0
- package/dist/i18n/i18n-context.d.ts +45 -0
- package/dist/i18n/i18n-context.js +65 -0
- package/dist/icon/icon-registry.d.ts +16 -0
- package/dist/icon/icon-registry.js +43 -0
- package/dist/icon/icon.d.ts +17 -0
- package/dist/icon/icon.js +23 -0
- package/dist/icon-button/icon-button.d.ts +23 -0
- package/dist/icon-button/icon-button.js +14 -0
- package/dist/icons-entry/public-api.d.ts +12 -0
- package/dist/icons-entry/public-api.js +20 -0
- package/dist/image/image.d.ts +17 -0
- package/dist/image/image.js +16 -0
- package/dist/input/input.d.ts +41 -0
- package/dist/input/input.js +36 -0
- package/dist/input-mask/input-mask.d.ts +19 -0
- package/dist/input-mask/input-mask.js +65 -0
- package/dist/input-number/input-number.d.ts +28 -0
- package/dist/input-number/input-number.js +52 -0
- package/dist/input-otp/input-otp.d.ts +21 -0
- package/dist/input-otp/input-otp.js +59 -0
- package/dist/internal/cn.d.ts +9 -0
- package/dist/internal/cn.js +19 -0
- package/dist/internal/compose-refs.d.ts +6 -0
- package/dist/internal/compose-refs.js +16 -0
- package/dist/internal/overlay-container.d.ts +3 -0
- package/dist/internal/overlay-container.js +19 -0
- package/dist/internal/overlay.d.ts +26 -0
- package/dist/internal/overlay.js +64 -0
- package/dist/internal/slot.d.ts +23 -0
- package/dist/internal/slot.js +49 -0
- package/dist/internal/types.d.ts +25 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/use-breakpoint.d.ts +18 -0
- package/dist/internal/use-breakpoint.js +52 -0
- package/dist/internal/use-controllable-state.d.ts +11 -0
- package/dist/internal/use-controllable-state.js +18 -0
- package/dist/internal/use-id.d.ts +9 -0
- package/dist/internal/use-id.js +13 -0
- package/dist/knob/knob.d.ts +27 -0
- package/dist/knob/knob.js +86 -0
- package/dist/listbox/listbox.d.ts +28 -0
- package/dist/listbox/listbox.js +43 -0
- package/dist/map/map-create-dialog.d.ts +21 -0
- package/dist/map/map-create-dialog.js +196 -0
- package/dist/map/map-hours-field.d.ts +22 -0
- package/dist/map/map-hours-field.js +90 -0
- package/dist/map/map-image-field.d.ts +24 -0
- package/dist/map/map-image-field.js +61 -0
- package/dist/map/map-legend.d.ts +29 -0
- package/dist/map/map-legend.js +41 -0
- package/dist/map/map-poi-card.d.ts +42 -0
- package/dist/map/map-poi-card.js +152 -0
- package/dist/map/map-search.d.ts +37 -0
- package/dist/map/map-search.js +201 -0
- package/dist/map/map.d.ts +113 -0
- package/dist/map/map.js +425 -0
- package/dist/map/ports.d.ts +76 -0
- package/dist/map/ports.js +174 -0
- package/dist/map-entry/public-api.d.ts +20 -0
- package/dist/map-entry/public-api.js +19 -0
- package/dist/megamenu/megamenu.d.ts +17 -0
- package/dist/megamenu/megamenu.js +28 -0
- package/dist/menu/menu-item.d.ts +33 -0
- package/dist/menu/menu-item.js +1 -0
- package/dist/menu/menu-rows.d.ts +9 -0
- package/dist/menu/menu-rows.js +15 -0
- package/dist/menu/menu.d.ts +22 -0
- package/dist/menu/menu.js +31 -0
- package/dist/menubar/menubar.d.ts +16 -0
- package/dist/menubar/menubar.js +39 -0
- package/dist/meter-group/meter-group.d.ts +20 -0
- package/dist/meter-group/meter-group.js +24 -0
- package/dist/order-list/order-list.d.ts +18 -0
- package/dist/order-list/order-list.js +37 -0
- package/dist/paginator/paginator.d.ts +29 -0
- package/dist/paginator/paginator.js +45 -0
- package/dist/panel/panel.d.ts +31 -0
- package/dist/panel/panel.js +36 -0
- package/dist/password/password.d.ts +28 -0
- package/dist/password/password.js +37 -0
- package/dist/pick-list/pick-list.d.ts +28 -0
- package/dist/pick-list/pick-list.js +65 -0
- package/dist/popover/popover.d.ts +23 -0
- package/dist/popover/popover.js +27 -0
- package/dist/progress/progress.d.ts +19 -0
- package/dist/progress/progress.js +21 -0
- package/dist/public-api.d.ts +181 -0
- package/dist/public-api.js +105 -0
- package/dist/radio-group/radio-group.d.ts +24 -0
- package/dist/radio-group/radio-group.js +23 -0
- package/dist/rating/rating.d.ts +22 -0
- package/dist/rating/rating.js +30 -0
- package/dist/rich-text/lists.d.ts +17 -0
- package/dist/rich-text/lists.js +144 -0
- package/dist/rich-text/rich-text.d.ts +28 -0
- package/dist/rich-text/rich-text.js +654 -0
- package/dist/rich-text/sanitize.d.ts +10 -0
- package/dist/rich-text/sanitize.js +85 -0
- package/dist/rich-text/tables.d.ts +44 -0
- package/dist/rich-text/tables.js +268 -0
- package/dist/roadmap/roadmap.d.ts +37 -0
- package/dist/roadmap/roadmap.js +271 -0
- package/dist/roadmap-entry/public-api.d.ts +4 -0
- package/dist/roadmap-entry/public-api.js +7 -0
- package/dist/scroll-top/scroll-top.d.ts +16 -0
- package/dist/scroll-top/scroll-top.js +38 -0
- package/dist/select/select.d.ts +37 -0
- package/dist/select/select.js +117 -0
- package/dist/shared/types.d.ts +8 -0
- package/dist/shared/types.js +1 -0
- package/dist/sidenav/sidenav.d.ts +26 -0
- package/dist/sidenav/sidenav.js +41 -0
- package/dist/skeleton/skeleton.d.ts +20 -0
- package/dist/skeleton/skeleton.js +15 -0
- package/dist/slider/slider.d.ts +28 -0
- package/dist/slider/slider.js +30 -0
- package/dist/speed-dial/speed-dial.d.ts +20 -0
- package/dist/speed-dial/speed-dial.js +23 -0
- package/dist/spinner/spinner.d.ts +15 -0
- package/dist/spinner/spinner.js +14 -0
- package/dist/split-button/split-button.d.ts +28 -0
- package/dist/split-button/split-button.js +34 -0
- package/dist/splitter/splitter.d.ts +39 -0
- package/dist/splitter/splitter.js +80 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +71 -0
- package/dist/switch/switch.d.ts +39 -0
- package/dist/switch/switch.js +30 -0
- package/dist/switch-group/switch-group.d.ts +29 -0
- package/dist/switch-group/switch-group.js +47 -0
- package/dist/tabs/tabs.d.ts +38 -0
- package/dist/tabs/tabs.js +74 -0
- package/dist/textarea/textarea.d.ts +28 -0
- package/dist/textarea/textarea.js +44 -0
- package/dist/timeline/timeline.d.ts +19 -0
- package/dist/timeline/timeline.js +12 -0
- package/dist/toast/toast-container.d.ts +12 -0
- package/dist/toast/toast-container.js +23 -0
- package/dist/toast/toast-store.d.ts +26 -0
- package/dist/toast/toast-store.js +51 -0
- package/dist/toolbar/toolbar.d.ts +29 -0
- package/dist/toolbar/toolbar.js +24 -0
- package/dist/tooltip/tooltip.d.ts +26 -0
- package/dist/tooltip/tooltip.js +113 -0
- package/dist/tree/tree.d.ts +31 -0
- package/dist/tree/tree.js +62 -0
- package/package.json +91 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { aggregate, isEmptyFilterValue, matchesColumnFilter, toText, } from '@konce-pt/table';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
|
+
import { KptButton } from "../button/button.js";
|
|
8
|
+
import { KptDatepicker } from "../datepicker/datepicker.js";
|
|
9
|
+
import { KptDateRange } from "../date-range/date-range.js";
|
|
10
|
+
import { KptIcon } from "../icon/icon.js";
|
|
11
|
+
import { KptInput } from "../input/input.js";
|
|
12
|
+
import { KptInputNumber } from "../input-number/input-number.js";
|
|
13
|
+
import { KptPaginator } from "../paginator/paginator.js";
|
|
14
|
+
import { KptSelect } from "../select/select.js";
|
|
15
|
+
import { KptSlider } from "../slider/slider.js";
|
|
16
|
+
import { exportTableCsv } from "./export-csv.js";
|
|
17
|
+
const REM_PX = 16;
|
|
18
|
+
const CHECKBOX_FROZEN_W = 3; // rem
|
|
19
|
+
const EXPANDER_FROZEN_W = 2.5; // rem
|
|
20
|
+
const VIRTUAL_BUFFER = 6;
|
|
21
|
+
/** Edytory z panelem w nakładce — otwierane od razu i zatwierdzane na zamknięciu panelu. */
|
|
22
|
+
const OVERLAY_EDITORS = new Set(['date', 'select', 'multiselect']);
|
|
23
|
+
/** Tablice (multiselect) porównujemy po zawartości, resztę po referencji. */
|
|
24
|
+
function sameEditValue(a, b) {
|
|
25
|
+
if (a === b)
|
|
26
|
+
return true;
|
|
27
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
28
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const EMPTY_ROWS = [];
|
|
32
|
+
const EMPTY_FIELDS = [];
|
|
33
|
+
/**
|
|
34
|
+
* Flagowa tabela danych: kolumny konfiguracją, sortowanie, filtr globalny i filtry kolumn,
|
|
35
|
+
* paginacja, zaznaczanie, kolumny zamrożone i przestawiane, edycja inline, wiersz podsumowania,
|
|
36
|
+
* rozwijane wiersze, wirtualizacja i eksport CSV.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* <KptDataTable columns={cols} data={rows} filterable pageSize={10} selectable="multiple" rowKey="id" />
|
|
40
|
+
*/
|
|
41
|
+
export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, filterRowPosition = 'bottom', pageSize = 0, pageSizeOptions = [5, 10, 20, 50], selectable = 'none', loading = false, emptyMessage, rowKey = null, scrollHeight = null, virtualScroll = false, rowHeight = 41, reorderable = false, exportable = false, exportFilename = 'export.csv', lazy = false, totalRecords = null, expandable = false, multiExpand = true, summaryLabel, summaryValues = null, selection, defaultSelection = EMPTY_ROWS, onSelectionChange, expandedRows, defaultExpandedRows = EMPTY_ROWS, onExpandedRowsChange, hiddenFields, defaultHiddenFields = EMPTY_FIELDS, onHiddenFieldsChange, onRowClick, onLazyLoad, onCellEdit, cells, cellEditors, renderRowDetail, paginatorActions, locale, dictionary, className, style, ...rest }) {
|
|
42
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
43
|
+
const [picked, setPicked] = useControllableState({
|
|
44
|
+
value: selection,
|
|
45
|
+
defaultValue: defaultSelection,
|
|
46
|
+
onChange: onSelectionChange,
|
|
47
|
+
});
|
|
48
|
+
const [expanded, setExpanded] = useControllableState({
|
|
49
|
+
value: expandedRows,
|
|
50
|
+
defaultValue: defaultExpandedRows,
|
|
51
|
+
onChange: onExpandedRowsChange,
|
|
52
|
+
});
|
|
53
|
+
const [hidden, setHidden] = useControllableState({
|
|
54
|
+
value: hiddenFields,
|
|
55
|
+
defaultValue: defaultHiddenFields,
|
|
56
|
+
onChange: onHiddenFieldsChange,
|
|
57
|
+
});
|
|
58
|
+
const [filterText, setFilterText] = useState('');
|
|
59
|
+
const [columnFilters, setColumnFilters] = useState({});
|
|
60
|
+
const [sortField, setSortField] = useState(null);
|
|
61
|
+
const [sortDir, setSortDir] = useState(null);
|
|
62
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
63
|
+
const [pageSizeState, setPageSizeState] = useState(pageSize || 10);
|
|
64
|
+
// Kolejność kolumn żyje w stanie (przestawianie), ale nowe `columns` z zewnątrz ją zastępują.
|
|
65
|
+
const [order, setOrder] = useState(columns);
|
|
66
|
+
const columnsRef = useRef(columns);
|
|
67
|
+
if (columnsRef.current !== columns) {
|
|
68
|
+
columnsRef.current = columns;
|
|
69
|
+
if (order !== columns)
|
|
70
|
+
setOrder(columns);
|
|
71
|
+
}
|
|
72
|
+
const [dragField, setDragField] = useState(null);
|
|
73
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
74
|
+
const [viewportHeight, setViewportHeight] = useState(400);
|
|
75
|
+
const [theadOffset, setTheadOffset] = useState(0);
|
|
76
|
+
const scrollRef = useRef(null);
|
|
77
|
+
const theadRef = useRef(null);
|
|
78
|
+
const editorHostRef = useRef(null);
|
|
79
|
+
const [editingCell, setEditingCell] = useState(null);
|
|
80
|
+
const [editingValue, setEditingValue] = useState(null);
|
|
81
|
+
const [editorPanelOpen, setEditorPanelOpen] = useState(false);
|
|
82
|
+
const visibleColumns = useMemo(() => order.filter((col) => !hidden.includes(col.field)), [order, hidden]);
|
|
83
|
+
const hasColumnFilters = columns.some((col) => col.filter);
|
|
84
|
+
const hasRowDetail = expandable && !!renderRowDetail;
|
|
85
|
+
const sameRow = (a, b) => rowKey ? a[rowKey] === b[rowKey] : a === b;
|
|
86
|
+
// ── Potok danych ──
|
|
87
|
+
const filtered = useMemo(() => {
|
|
88
|
+
const query = filterText.trim().toLowerCase();
|
|
89
|
+
let rows = data;
|
|
90
|
+
if (query) {
|
|
91
|
+
rows = rows.filter((row) => columns.some((col) => toText(row[col.field]).toLowerCase().includes(query)));
|
|
92
|
+
}
|
|
93
|
+
if (hasColumnFilters) {
|
|
94
|
+
rows = rows.filter((row) => columns.every((col) => {
|
|
95
|
+
if (!col.filter)
|
|
96
|
+
return true;
|
|
97
|
+
const value = columnFilters[col.field];
|
|
98
|
+
if (isEmptyFilterValue(value))
|
|
99
|
+
return true;
|
|
100
|
+
return matchesColumnFilter(col.filter, row[col.field], value);
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return rows;
|
|
104
|
+
}, [columnFilters, columns, data, filterText, hasColumnFilters]);
|
|
105
|
+
const sorted = useMemo(() => {
|
|
106
|
+
if (!sortField || !sortDir)
|
|
107
|
+
return filtered;
|
|
108
|
+
const sign = sortDir === 'asc' ? 1 : -1;
|
|
109
|
+
return [...filtered].sort((a, b) => {
|
|
110
|
+
const av = a[sortField];
|
|
111
|
+
const bv = b[sortField];
|
|
112
|
+
// Puste komórki lądują na końcu niezależnie od kierunku — sortowanie ma pokazywać dane.
|
|
113
|
+
if (av == null)
|
|
114
|
+
return 1;
|
|
115
|
+
if (bv == null)
|
|
116
|
+
return -1;
|
|
117
|
+
if (typeof av === 'number' && typeof bv === 'number')
|
|
118
|
+
return (av - bv) * sign;
|
|
119
|
+
return toText(av).localeCompare(toText(bv)) * sign;
|
|
120
|
+
});
|
|
121
|
+
}, [filtered, sortDir, sortField]);
|
|
122
|
+
const paged = useMemo(() => {
|
|
123
|
+
if (lazy)
|
|
124
|
+
return data;
|
|
125
|
+
if (pageSize <= 0)
|
|
126
|
+
return sorted;
|
|
127
|
+
const start = pageIndex * pageSizeState;
|
|
128
|
+
return sorted.slice(start, start + pageSizeState);
|
|
129
|
+
}, [data, lazy, pageIndex, pageSize, pageSizeState, sorted]);
|
|
130
|
+
const filteredCount = lazy ? (totalRecords ?? data.length) : filtered.length;
|
|
131
|
+
// Tryb lazy: każda zmiana stanu to prośba do rodzica o dane.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!lazy)
|
|
134
|
+
return;
|
|
135
|
+
onLazyLoad?.({ pageIndex, pageSize: pageSizeState, sortField, sortDir, filter: filterText, columnFilters });
|
|
136
|
+
// `onLazyLoad` celowo poza zależnościami: nowa referencja funkcji przy każdym renderze
|
|
137
|
+
// rodzica wywołałaby pętlę żądań.
|
|
138
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
139
|
+
}, [lazy, pageIndex, pageSizeState, sortField, sortDir, filterText, columnFilters]);
|
|
140
|
+
// Offset sticky drugiego wiersza nagłówka = wysokość pierwszego.
|
|
141
|
+
useLayoutEffect(() => {
|
|
142
|
+
const first = theadRef.current?.rows?.[0];
|
|
143
|
+
if (first)
|
|
144
|
+
setTheadOffset(first.offsetHeight);
|
|
145
|
+
}, [filterRowPosition, hasColumnFilters, visibleColumns]);
|
|
146
|
+
useLayoutEffect(() => {
|
|
147
|
+
const el = scrollRef.current;
|
|
148
|
+
if (el)
|
|
149
|
+
setViewportHeight(el.clientHeight);
|
|
150
|
+
}, []);
|
|
151
|
+
// Wejście w edycję → fokus na kontrolce edytora.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!editingCell)
|
|
154
|
+
return;
|
|
155
|
+
editorHostRef.current?.querySelector('input, .kpt-select, [tabindex]')?.focus();
|
|
156
|
+
}, [editingCell]);
|
|
157
|
+
// ── Wirtualizacja ──
|
|
158
|
+
const virtualStart = Math.max(0, Math.floor(scrollTop / rowHeight) - VIRTUAL_BUFFER);
|
|
159
|
+
const virtualEnd = Math.min(sorted.length, virtualStart + Math.ceil(viewportHeight / rowHeight) + VIRTUAL_BUFFER * 2);
|
|
160
|
+
const virtualRows = sorted.slice(virtualStart, virtualEnd);
|
|
161
|
+
const topPad = virtualStart * rowHeight;
|
|
162
|
+
const bottomPad = (sorted.length - virtualEnd) * rowHeight;
|
|
163
|
+
const onScroll = (event) => {
|
|
164
|
+
if (!virtualScroll)
|
|
165
|
+
return;
|
|
166
|
+
const el = event.currentTarget;
|
|
167
|
+
setScrollTop(el.scrollTop);
|
|
168
|
+
setViewportHeight(el.clientHeight);
|
|
169
|
+
};
|
|
170
|
+
// ── Kolumny zamrożone ──
|
|
171
|
+
const parseWidthPx = (width) => {
|
|
172
|
+
if (!width)
|
|
173
|
+
return 8 * REM_PX;
|
|
174
|
+
const n = Number.parseFloat(width);
|
|
175
|
+
if (Number.isNaN(n))
|
|
176
|
+
return 8 * REM_PX;
|
|
177
|
+
return width.trim().endsWith('px') ? n : n * REM_PX;
|
|
178
|
+
};
|
|
179
|
+
const leadingCells = (selectable !== 'none' ? 1 : 0) + (hasRowDetail ? 1 : 0);
|
|
180
|
+
const leadingStickyWidth = (hasRowDetail ? EXPANDER_FROZEN_W * REM_PX : 0) + (selectable !== 'none' ? CHECKBOX_FROZEN_W * REM_PX : 0);
|
|
181
|
+
const frozenOffsets = useMemo(() => {
|
|
182
|
+
const map = new Map();
|
|
183
|
+
let acc = leadingStickyWidth;
|
|
184
|
+
for (const col of visibleColumns) {
|
|
185
|
+
if (!col.frozen)
|
|
186
|
+
break; // tylko wiodące, sąsiadujące
|
|
187
|
+
map.set(col.field, acc);
|
|
188
|
+
acc += parseWidthPx(col.width);
|
|
189
|
+
}
|
|
190
|
+
return map;
|
|
191
|
+
}, [leadingStickyWidth, visibleColumns]);
|
|
192
|
+
const hasFrozen = frozenOffsets.size > 0;
|
|
193
|
+
const frozenLeft = (field) => frozenOffsets.get(field);
|
|
194
|
+
const expanderStickyLeft = hasFrozen ? 0 : undefined;
|
|
195
|
+
const checkboxStickyLeft = hasFrozen ? (hasRowDetail ? EXPANDER_FROZEN_W * REM_PX : 0) : undefined;
|
|
196
|
+
// ── Przestawianie kolumn ──
|
|
197
|
+
const frozenLeadCount = (() => {
|
|
198
|
+
let n = 0;
|
|
199
|
+
for (const col of visibleColumns) {
|
|
200
|
+
if (col.frozen)
|
|
201
|
+
n++;
|
|
202
|
+
else
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return n;
|
|
206
|
+
})();
|
|
207
|
+
const isColDraggable = (col) => reorderable && !col.frozen;
|
|
208
|
+
const onColDrop = (field, event) => {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
if (dragField === null || dragField === field)
|
|
211
|
+
return;
|
|
212
|
+
const visIndex = visibleColumns.findIndex((col) => col.field === field);
|
|
213
|
+
if (visIndex < frozenLeadCount)
|
|
214
|
+
return; // nie wsuwaj przed kolumny zamrożone
|
|
215
|
+
const cols = [...order];
|
|
216
|
+
const fromIdx = cols.findIndex((col) => col.field === dragField);
|
|
217
|
+
const toIdx = cols.findIndex((col) => col.field === field);
|
|
218
|
+
if (fromIdx === -1 || toIdx === -1)
|
|
219
|
+
return;
|
|
220
|
+
const [moved] = cols.splice(fromIdx, 1);
|
|
221
|
+
cols.splice(toIdx, 0, moved);
|
|
222
|
+
setOrder(cols);
|
|
223
|
+
setDragField(null);
|
|
224
|
+
};
|
|
225
|
+
// ── Sortowanie ──
|
|
226
|
+
const toggleSort = (col) => {
|
|
227
|
+
if (!col.sortable)
|
|
228
|
+
return;
|
|
229
|
+
if (sortField !== col.field) {
|
|
230
|
+
setSortField(col.field);
|
|
231
|
+
setSortDir('asc');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (sortDir === 'asc')
|
|
235
|
+
setSortDir('desc');
|
|
236
|
+
else if (sortDir === 'desc') {
|
|
237
|
+
setSortField(null);
|
|
238
|
+
setSortDir(null);
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
setSortDir('asc');
|
|
242
|
+
};
|
|
243
|
+
const ariaSort = (col) => {
|
|
244
|
+
if (sortField !== col.field || !sortDir)
|
|
245
|
+
return 'none';
|
|
246
|
+
return sortDir === 'asc' ? 'ascending' : 'descending';
|
|
247
|
+
};
|
|
248
|
+
// ── Filtry kolumn ──
|
|
249
|
+
const setColumnFilter = (field, value) => {
|
|
250
|
+
setColumnFilters((current) => ({ ...current, [field]: value }));
|
|
251
|
+
setPageIndex(0);
|
|
252
|
+
};
|
|
253
|
+
const rangeVal = (field) => {
|
|
254
|
+
const value = columnFilters[field];
|
|
255
|
+
return { min: value?.min ?? '', max: value?.max ?? '' };
|
|
256
|
+
};
|
|
257
|
+
const sliderVal = (col) => {
|
|
258
|
+
const min = col.filter?.min ?? 0;
|
|
259
|
+
const max = col.filter?.max ?? 100;
|
|
260
|
+
return columnFilters[col.field] ?? [min, max];
|
|
261
|
+
};
|
|
262
|
+
// ── Zaznaczanie ──
|
|
263
|
+
const isSelected = (row) => picked.some((r) => sameRow(r, row));
|
|
264
|
+
const toggleRow = (row) => {
|
|
265
|
+
if (selectable === 'none')
|
|
266
|
+
return;
|
|
267
|
+
if (selectable === 'single') {
|
|
268
|
+
setPicked(isSelected(row) ? [] : [row]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setPicked(isSelected(row) ? picked.filter((r) => !sameRow(r, row)) : [...picked, row]);
|
|
272
|
+
};
|
|
273
|
+
const pageAllSelected = paged.length > 0 && paged.every((row) => isSelected(row));
|
|
274
|
+
const pageSomeSelected = paged.some((row) => isSelected(row)) && !pageAllSelected;
|
|
275
|
+
const toggleAllOnPage = () => {
|
|
276
|
+
if (pageAllSelected)
|
|
277
|
+
setPicked(picked.filter((r) => !paged.some((pr) => sameRow(pr, r))));
|
|
278
|
+
else
|
|
279
|
+
setPicked([...picked, ...paged.filter((row) => !isSelected(row))]);
|
|
280
|
+
};
|
|
281
|
+
// ── Rozwijane wiersze ──
|
|
282
|
+
const isExpanded = (row) => expanded.some((r) => sameRow(r, row));
|
|
283
|
+
const toggleExpand = (row) => {
|
|
284
|
+
if (isExpanded(row))
|
|
285
|
+
setExpanded(expanded.filter((r) => !sameRow(r, row)));
|
|
286
|
+
else
|
|
287
|
+
setExpanded(multiExpand ? [...expanded, row] : [row]);
|
|
288
|
+
};
|
|
289
|
+
// ── Edycja inline ──
|
|
290
|
+
const isEditable = (col) => !!col.edit || !!cellEditors?.[col.field];
|
|
291
|
+
const isEditingCell = (row, field) => !!editingCell && editingCell.field === field && sameRow(editingCell.row, row);
|
|
292
|
+
const startEdit = (row, col, event) => {
|
|
293
|
+
event.stopPropagation();
|
|
294
|
+
if (!isEditable(col) || isEditingCell(row, col.field))
|
|
295
|
+
return;
|
|
296
|
+
setEditingCell({ row, field: col.field });
|
|
297
|
+
setEditingValue(row[col.field]);
|
|
298
|
+
// Edytory z panelem otwieramy od razu: ich zamknięcie jest jedynym momentem, w którym
|
|
299
|
+
// wiadomo, że użytkownik skończył wybierać.
|
|
300
|
+
setEditorPanelOpen(!!col.edit && OVERLAY_EDITORS.has(col.edit.type));
|
|
301
|
+
};
|
|
302
|
+
const commitEdit = (value = editingValue) => {
|
|
303
|
+
if (!editingCell)
|
|
304
|
+
return;
|
|
305
|
+
const { row, field } = editingCell;
|
|
306
|
+
setEditingCell(null);
|
|
307
|
+
setEditorPanelOpen(false);
|
|
308
|
+
if (sameEditValue(value, row[field]))
|
|
309
|
+
return;
|
|
310
|
+
onCellEdit?.({ row, field, value, updatedRow: { ...row, [field]: value } });
|
|
311
|
+
};
|
|
312
|
+
const cancelEdit = () => {
|
|
313
|
+
setEditingCell(null);
|
|
314
|
+
setEditorPanelOpen(false);
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Edytory bez panelu (text/number, własne) zatwierdza dopiero wyjście fokusu poza CAŁY edytor:
|
|
318
|
+
* `KptInputNumber` ma fokusowalne strzałki obok pola, więc reakcja na blur samej kontrolki
|
|
319
|
+
* zamykałaby edycję przy każdym kliknięciu w „+".
|
|
320
|
+
*/
|
|
321
|
+
const onEditorFocusOut = (event) => {
|
|
322
|
+
if (editorPanelOpen)
|
|
323
|
+
return; // edytory z panelem zatwierdza jego zamknięcie
|
|
324
|
+
const host = editorHostRef.current;
|
|
325
|
+
const next = event.relatedTarget;
|
|
326
|
+
if (host && next && host.contains(next))
|
|
327
|
+
return; // fokus został w edytorze
|
|
328
|
+
commitEdit();
|
|
329
|
+
};
|
|
330
|
+
const onEditorPanelToggle = (open) => {
|
|
331
|
+
setEditorPanelOpen(open);
|
|
332
|
+
if (!open)
|
|
333
|
+
commitEdit();
|
|
334
|
+
};
|
|
335
|
+
// ── Wiersz podsumowania ──
|
|
336
|
+
const summarySource = lazy ? EMPTY_ROWS : filtered;
|
|
337
|
+
const overrides = summaryValues ?? {};
|
|
338
|
+
const summaryFields = visibleColumns
|
|
339
|
+
.filter((col) => col.summary || col.field in overrides)
|
|
340
|
+
.map((col) => col.field);
|
|
341
|
+
const hasSummary = summaryFields.length > 0;
|
|
342
|
+
const summaryStartIndex = summaryFields[0] === undefined ? -1 : visibleColumns.findIndex((col) => col.field === summaryFields[0]);
|
|
343
|
+
const summaryLabelSpan = leadingCells + summaryStartIndex;
|
|
344
|
+
const formatSummary = (raw, format) => format ? format(raw) : toText(raw);
|
|
345
|
+
const summaryText = (col) => {
|
|
346
|
+
if (col.field in overrides)
|
|
347
|
+
return formatSummary(overrides[col.field], col.summary?.format);
|
|
348
|
+
if (!col.summary)
|
|
349
|
+
return '';
|
|
350
|
+
const raw = typeof col.summary.fn === 'function'
|
|
351
|
+
? col.summary.fn(summarySource)
|
|
352
|
+
: aggregate(summarySource, col.field, col.summary.fn);
|
|
353
|
+
return formatSummary(raw, col.summary.format);
|
|
354
|
+
};
|
|
355
|
+
const colspan = visibleColumns.length + leadingCells;
|
|
356
|
+
const displayCount = virtualScroll ? sorted.length : paged.length;
|
|
357
|
+
const rowsToRender = virtualScroll ? virtualRows : paged;
|
|
358
|
+
// ── Renderowanie ──
|
|
359
|
+
const filterCell = (col) => {
|
|
360
|
+
const filter = col.filter;
|
|
361
|
+
if (!filter)
|
|
362
|
+
return null;
|
|
363
|
+
const stop = (event) => event.stopPropagation();
|
|
364
|
+
switch (filter.type) {
|
|
365
|
+
case 'text':
|
|
366
|
+
return (_jsx("input", { className: "kpt-data-table__fcell", type: "search", placeholder: filter.placeholder || '', "aria-label": t('dataTable.filter', { column: col.header }), value: columnFilters[col.field] || '', onChange: (event) => setColumnFilter(col.field, event.target.value), onClick: stop }));
|
|
367
|
+
case 'number':
|
|
368
|
+
return (_jsx("input", { className: "kpt-data-table__fcell", type: "text", inputMode: "numeric", placeholder: filter.placeholder || '', "aria-label": t('dataTable.filter', { column: col.header }), value: columnFilters[col.field] || '', onChange: (event) => setColumnFilter(col.field, event.target.value.replace(/\D/g, '')), onClick: stop }));
|
|
369
|
+
case 'select':
|
|
370
|
+
return (_jsx(KptSelect, { options: filter.options ?? [], multiple: !!filter.multiple, filter: true, placeholder: filter.placeholder || t('dataTable.all'), value: columnFilters[col.field] ?? null, onValueChange: (value) => setColumnFilter(col.field, value) }));
|
|
371
|
+
case 'numberRange':
|
|
372
|
+
return (_jsxs("div", { className: "kpt-data-table__frange", children: [_jsx("input", { className: "kpt-data-table__fcell", type: "text", inputMode: "numeric", placeholder: t('dataTable.min'), "aria-label": t('dataTable.filterMin', { column: col.header }), value: rangeVal(col.field).min, onChange: (event) => setColumnFilter(col.field, { ...rangeVal(col.field), min: event.target.value.replace(/\D/g, '') }), onClick: stop }), _jsx("input", { className: "kpt-data-table__fcell", type: "text", inputMode: "numeric", placeholder: t('dataTable.max'), "aria-label": t('dataTable.filterMax', { column: col.header }), value: rangeVal(col.field).max, onChange: (event) => setColumnFilter(col.field, { ...rangeVal(col.field), max: event.target.value.replace(/\D/g, '') }), onClick: stop })] }));
|
|
373
|
+
case 'slider':
|
|
374
|
+
return (_jsx("div", { className: "kpt-data-table__fslider", onClick: stop, children: _jsx(KptSlider, { range: true, min: filter.min ?? 0, max: filter.max ?? 100, step: filter.step ?? 1, value: sliderVal(col), onValueChange: (value) => setColumnFilter(col.field, value), showValue: true }) }));
|
|
375
|
+
case 'dateRange':
|
|
376
|
+
return (_jsx(KptDateRange, { placeholder: filter.placeholder || t('dataTable.dateRange'), value: columnFilters[col.field] || { start: '', end: '' }, onValueChange: (value) => setColumnFilter(col.field, value) }));
|
|
377
|
+
default:
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const filterRow = (_jsxs("tr", { className: "kpt-data-table__filter-row", children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable !== 'none' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, visibleColumns.map((col) => (_jsx("th", { style: { width: col.width, left: frozenLeft(col.field) }, "data-align": col.align ?? 'start', className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen'), children: filterCell(col) }, col.field)))] }));
|
|
382
|
+
const editor = (row, col) => {
|
|
383
|
+
const custom = cellEditors?.[col.field];
|
|
384
|
+
if (custom) {
|
|
385
|
+
const context = {
|
|
386
|
+
value: editingValue,
|
|
387
|
+
row,
|
|
388
|
+
commit: (value) => commitEdit(value),
|
|
389
|
+
cancel: cancelEdit,
|
|
390
|
+
};
|
|
391
|
+
return custom(context);
|
|
392
|
+
}
|
|
393
|
+
const edit = col.edit;
|
|
394
|
+
if (!edit)
|
|
395
|
+
return null;
|
|
396
|
+
switch (edit.type) {
|
|
397
|
+
case 'text':
|
|
398
|
+
return (_jsx(KptInput, { value: editingValue ?? '', placeholder: edit.placeholder ?? '', onValueChange: setEditingValue, onKeyDown: (event) => {
|
|
399
|
+
if (event.key === 'Enter')
|
|
400
|
+
commitEdit();
|
|
401
|
+
if (event.key === 'Escape')
|
|
402
|
+
cancelEdit();
|
|
403
|
+
} }));
|
|
404
|
+
case 'number':
|
|
405
|
+
return (_jsx(KptInputNumber, { value: editingValue, min: edit.min, max: edit.max, step: edit.step ?? 1, onValueChange: setEditingValue, onKeyDown: (event) => {
|
|
406
|
+
if (event.key === 'Enter')
|
|
407
|
+
commitEdit();
|
|
408
|
+
if (event.key === 'Escape')
|
|
409
|
+
cancelEdit();
|
|
410
|
+
} }));
|
|
411
|
+
case 'date':
|
|
412
|
+
return (_jsx(KptDatepicker, { value: editingValue ?? '', onValueChange: setEditingValue, open: editorPanelOpen, onOpenChange: onEditorPanelToggle }));
|
|
413
|
+
case 'select':
|
|
414
|
+
case 'multiselect':
|
|
415
|
+
return (_jsx(KptSelect, { multiple: edit.type === 'multiselect', filter: edit.type === 'multiselect', options: edit.options ?? [], placeholder: edit.placeholder ?? '', value: editingValue, onValueChange: setEditingValue, open: editorPanelOpen, onOpenChange: onEditorPanelToggle }));
|
|
416
|
+
default:
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const renderRow = (row, index) => (_jsxs("tbody", { style: { display: 'contents' }, children: [_jsxs("tr", { className: cn(isSelected(row) && 'kpt-data-table__row--selected', hasRowDetail && isExpanded(row) && 'kpt-data-table__row--expanded'), style: { height: virtualScroll ? rowHeight : undefined }, onClick: () => onRowClick?.(row), children: [hasRowDetail ? (_jsx("td", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft }, children: _jsx("button", { type: "button", className: cn('kpt-data-table__expander-btn', isExpanded(row) && 'kpt-data-table__expander-btn--open'), "aria-expanded": isExpanded(row), "aria-label": t('dataTable.expandRow'), onClick: (event) => {
|
|
421
|
+
event.stopPropagation();
|
|
422
|
+
toggleExpand(row);
|
|
423
|
+
}, children: _jsx(KptIcon, { name: "chevron-right" }) }) })) : null, selectable !== 'none' ? (_jsx("td", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: "checkbox", checked: isSelected(row), "aria-label": t('dataTable.selectRow'), onClick: (event) => event.stopPropagation(), onChange: () => toggleRow(row) }) })) : null, visibleColumns.map((col) => {
|
|
424
|
+
const editing = isEditingCell(row, col.field);
|
|
425
|
+
const editable = isEditable(col) && !editing;
|
|
426
|
+
return (_jsx("td", { "data-align": col.align ?? 'start', style: { left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', editable && 'kpt-data-table__cell--editable'), tabIndex: editable ? 0 : undefined, "aria-label": editable ? t('dataTable.editCell', { column: col.header }) : undefined, onClick: (event) => {
|
|
427
|
+
if (isEditable(col))
|
|
428
|
+
startEdit(row, col, event);
|
|
429
|
+
}, onKeyDown: (event) => {
|
|
430
|
+
if (event.key === 'Enter' && editable)
|
|
431
|
+
startEdit(row, col, event);
|
|
432
|
+
}, children: editing ? (_jsx("div", { ref: editorHostRef, className: "kpt-data-table__editor", onClick: (event) => event.stopPropagation(), onBlur: onEditorFocusOut, children: editor(row, col) })) : cells?.[col.field] ? (cells[col.field](row, row[col.field])) : row[col.field] }, col.field));
|
|
433
|
+
})] }), hasRowDetail && isExpanded(row) && !virtualScroll ? (_jsx("tr", { className: "kpt-data-table__detail-row", children: _jsx("td", { className: "kpt-data-table__detail", colSpan: colspan, children: renderRowDetail?.(row) }) })) : null] }, rowKey ? String(row[rowKey]) : index));
|
|
434
|
+
return (_jsxs("div", { className: cn('kpt-data-table', className), style: { ['--kpt-thead-offset']: `${theadOffset}px`, ...style }, ...rest, children: [filterable || exportable ? (_jsxs("div", { className: "kpt-data-table__toolbar", children: [filterable ? (_jsx("input", { className: "kpt-data-table__filter", type: "search", placeholder: t('dataTable.search'), value: filterText, onChange: (event) => {
|
|
435
|
+
setFilterText(event.target.value);
|
|
436
|
+
setPageIndex(0);
|
|
437
|
+
} })) : null, exportable ? (_jsx(KptButton, { variant: "outline", size: "sm", onClick: () => exportTableCsv(visibleColumns, (lazy ? data : sorted), exportFilename), children: t('dataTable.exportCsv') })) : null] })) : null, _jsx("div", { ref: scrollRef, className: "kpt-data-table__scroll", style: { maxHeight: scrollHeight ?? undefined }, onScroll: onScroll, children: _jsxs("table", { className: cn('kpt-data-table__table', hasFrozen && 'kpt-data-table__table--frozen', virtualScroll && 'kpt-data-table__table--virtual'), children: [_jsxs("thead", { ref: theadRef, children: [hasColumnFilters && filterRowPosition === 'top' ? filterRow : null, _jsxs("tr", { children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable === 'multiple' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: "checkbox", checked: pageAllSelected, ref: (el) => {
|
|
438
|
+
if (el)
|
|
439
|
+
el.indeterminate = pageSomeSelected;
|
|
440
|
+
}, "aria-label": t('dataTable.selectAll'), onChange: toggleAllOnPage }) })) : selectable === 'single' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, visibleColumns.map((col) => (_jsxs("th", { draggable: isColDraggable(col) || undefined, style: { width: col.width, left: frozenLeft(col.field) }, "data-align": col.align ?? 'start', className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', col.sortable && 'kpt-data-table__th--sortable', isColDraggable(col) && 'kpt-data-table__th--draggable', dragField !== null && dragField !== col.field && !col.frozen && 'kpt-data-table__th--dragover'), "aria-sort": ariaSort(col), onClick: () => toggleSort(col), onDragStart: (event) => {
|
|
441
|
+
if (col.frozen)
|
|
442
|
+
return;
|
|
443
|
+
setDragField(col.field);
|
|
444
|
+
event.dataTransfer.setData('text/plain', col.field);
|
|
445
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
446
|
+
}, onDragOver: (event) => {
|
|
447
|
+
if (!reorderable)
|
|
448
|
+
return;
|
|
449
|
+
const visIndex = visibleColumns.findIndex((c) => c.field === col.field);
|
|
450
|
+
if (dragField !== null && visIndex >= frozenLeadCount)
|
|
451
|
+
event.preventDefault();
|
|
452
|
+
}, onDrop: (event) => reorderable && onColDrop(col.field, event), onDragEnd: () => setDragField(null), children: [isColDraggable(col) ? (_jsx(KptIcon, { className: "kpt-data-table__grip", name: "grip-vertical" })) : null, _jsx("span", { children: col.header }), col.sortable ? (_jsx(KptIcon, { className: cn('kpt-data-table__sort', ariaSort(col) !== 'none' && 'kpt-data-table__sort--active'), name: sortField === col.field && sortDir === 'asc' ? 'chevron-up' : 'chevron-down' })) : null] }, col.field)))] }), hasColumnFilters && filterRowPosition === 'bottom' ? filterRow : null] }), loading ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: t('dataTable.loading') }) }) })) : displayCount === 0 ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: emptyMessage ?? t('dataTable.empty') }) }) })) : (_jsxs(_Fragment, { children: [virtualScroll && topPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: topPad } }) }) })) : null, rowsToRender.map((row, index) => renderRow(row, index)), virtualScroll && bottomPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: bottomPad } }) }) })) : null] })), hasSummary && !loading && displayCount > 0 ? (_jsx("tfoot", { children: _jsxs("tr", { className: "kpt-data-table__summary", children: [summaryLabelSpan > 0 ? (_jsx("th", { className: cn('kpt-data-table__summary-label', hasFrozen && 'kpt-data-table__cell--frozen'), colSpan: summaryLabelSpan, style: { left: hasFrozen ? 0 : undefined }, children: summaryLabel ?? t('dataTable.total') })) : null, visibleColumns.map((col, index) => index >= summaryStartIndex ? (_jsx("td", { "data-align": col.align ?? 'end', style: { width: col.width, left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen'), children: summaryText(col) }, col.field)) : null)] }) })) : null] }) }), pageSize > 0 && !virtualScroll ? (_jsxs("div", { className: "kpt-data-table__pager", children: [paginatorActions, _jsx(KptPaginator, { length: filteredCount, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: pageSizeState, onPageSizeChange: setPageSizeState, pageSizeOptions: pageSizeOptions })] })) : null] }));
|
|
453
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KptColumn } from './data-table-types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Zrzuca wiersze do pliku CSV (separator `;`). Tabela woła to samo z przycisku „Eksport CSV";
|
|
4
|
+
* funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
|
|
5
|
+
* — eksport z kodu aplikacji wywołuje się właśnie tak.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exportTableCsv(columns: readonly KptColumn[], rows: readonly Record<string, unknown>[], filename?: string): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { escapeCsvCell } from '@konce-pt/table';
|
|
2
|
+
/**
|
|
3
|
+
* Zrzuca wiersze do pliku CSV (separator `;`). Tabela woła to samo z przycisku „Eksport CSV";
|
|
4
|
+
* funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
|
|
5
|
+
* — eksport z kodu aplikacji wywołuje się właśnie tak.
|
|
6
|
+
*/
|
|
7
|
+
export function exportTableCsv(columns, rows, filename = 'export.csv') {
|
|
8
|
+
const header = columns.map((col) => escapeCsvCell(col.header)).join(';');
|
|
9
|
+
const body = rows.map((row) => columns.map((col) => escapeCsvCell(row[col.field])).join(';')).join('\n');
|
|
10
|
+
// BOM — bez niego Excel czyta UTF-8 jako stronę kodową systemu i psuje polskie znaki.
|
|
11
|
+
const csv = `${header}\n${body}`;
|
|
12
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
13
|
+
const url = URL.createObjectURL(blob);
|
|
14
|
+
const link = document.createElement('a');
|
|
15
|
+
link.href = url;
|
|
16
|
+
link.download = filename;
|
|
17
|
+
link.click();
|
|
18
|
+
URL.revokeObjectURL(url);
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
3
|
+
export type KptDataViewLayout = 'list' | 'grid';
|
|
4
|
+
export interface KptDataViewProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
|
|
5
|
+
items?: T[];
|
|
6
|
+
layout?: KptDataViewLayout;
|
|
7
|
+
defaultLayout?: KptDataViewLayout;
|
|
8
|
+
onLayoutChange?: (layout: KptDataViewLayout) => void;
|
|
9
|
+
/** Rozmiar strony; 0 wyłącza stronicowanie. */
|
|
10
|
+
pageSize?: number;
|
|
11
|
+
paginated?: boolean;
|
|
12
|
+
/** Szablon elementu — odpowiednik `<ng-template let-item>` z Angulara. */
|
|
13
|
+
renderItem: (item: T, index: number) => ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* DataView — lista elementów w układzie listy albo siatki, z opcjonalnym stronicowaniem.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <KptDataView items={products} pageSize={6} renderItem={(p) => <div>{p.name}</div>} />
|
|
20
|
+
*/
|
|
21
|
+
export declare function KptDataView<T>({ items, layout, defaultLayout, onLayoutChange, pageSize, paginated, renderItem, locale, dictionary, className, ...rest }: Readonly<KptDataViewProps<T>>): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
|
+
import { KptIcon } from "../icon/icon.js";
|
|
7
|
+
import { KptPaginator } from "../paginator/paginator.js";
|
|
8
|
+
/**
|
|
9
|
+
* DataView — lista elementów w układzie listy albo siatki, z opcjonalnym stronicowaniem.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <KptDataView items={products} pageSize={6} renderItem={(p) => <div>{p.name}</div>} />
|
|
13
|
+
*/
|
|
14
|
+
export function KptDataView({ items = [], layout, defaultLayout = 'grid', onLayoutChange, pageSize = 0, paginated = true, renderItem, locale, dictionary, className, ...rest }) {
|
|
15
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
16
|
+
const [currentLayout, setLayout] = useControllableState({
|
|
17
|
+
value: layout,
|
|
18
|
+
defaultValue: defaultLayout,
|
|
19
|
+
onChange: onLayoutChange,
|
|
20
|
+
});
|
|
21
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
22
|
+
const showPager = paginated && pageSize > 0 && items.length > pageSize;
|
|
23
|
+
const pageItems = !paginated || pageSize <= 0 ? items : items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
|
|
24
|
+
return (_jsx("div", { className: cn('kpt-data-view-host', className), ...rest, children: _jsxs("div", { className: "kpt-data-view", children: [_jsxs("div", { className: "kpt-data-view__toolbar", children: [_jsx("div", { className: "kpt-data-view__count", children: t('dataView.count', { count: items.length }) }), _jsxs("div", { className: "kpt-data-view__switch", children: [_jsx("button", { type: "button", className: cn(currentLayout === 'list' && 'is-active'), "aria-label": t('dataView.list'), onClick: () => setLayout('list'), children: _jsx(KptIcon, { name: "list" }) }), _jsx("button", { type: "button", className: cn(currentLayout === 'grid' && 'is-active'), "aria-label": t('dataView.grid'), onClick: () => setLayout('grid'), children: _jsx(KptIcon, { name: "menu" }) })] })] }), _jsx("div", { className: "kpt-data-view__content", "data-layout": currentLayout, children: pageItems.length ? (pageItems.map((item, index) => (_jsx("div", { className: "kpt-data-view__item", children: renderItem(item, index) }, index)))) : (_jsx("div", { className: "kpt-data-view__empty", children: t('dataView.empty') })) }), showPager ? (_jsx(KptPaginator, { length: items.length, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: pageSize, pageSizeOptions: [pageSize] })) : null] }) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptDateRangeMode, type KptDateRangeValue } from '@konce-pt/datetime';
|
|
3
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
4
|
+
export type { KptDateRangeMode, KptDateRangeValue };
|
|
5
|
+
export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange' | 'defaultValue'>, KptI18nOverrides {
|
|
6
|
+
value?: KptDateRangeValue;
|
|
7
|
+
defaultValue?: KptDateRangeValue;
|
|
8
|
+
onValueChange?: (value: KptDateRangeValue) => void;
|
|
9
|
+
mode?: KptDateRangeMode;
|
|
10
|
+
/** Liczba miesięcy obok siebie (1–3). Ignorowana w trybie 'time'. */
|
|
11
|
+
visibleMonths?: number;
|
|
12
|
+
hourFormat?: 12 | 24;
|
|
13
|
+
minuteStep?: number;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
displayFormat?: 'pl' | 'iso';
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
invalid?: boolean;
|
|
18
|
+
touched?: boolean;
|
|
19
|
+
onTouch?: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wybór zakresu dat (i godzin) z kalendarzem. Pierwszy klik ustawia początek, drugi koniec;
|
|
23
|
+
* `visibleMonths` pokazuje kilka miesięcy obok siebie, a zakres podświetla się ciągłym pasmem
|
|
24
|
+
* przez wszystkie panele.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
|
|
28
|
+
* <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
|
|
29
|
+
*/
|
|
30
|
+
export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
|