xontable 0.1.1
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 +29 -0
- package/dist/XOnTable.d.ts +4 -0
- package/dist/XOnTable.d.ts.map +1 -0
- package/dist/XOnTable.js +182 -0
- package/dist/components/ColumnFilterMenu.d.ts +13 -0
- package/dist/components/ColumnFilterMenu.d.ts.map +1 -0
- package/dist/components/ColumnFilterMenu.js +7 -0
- package/dist/components/SelectMenu.d.ts +15 -0
- package/dist/components/SelectMenu.d.ts.map +1 -0
- package/dist/components/SelectMenu.js +38 -0
- package/dist/components/XOnTableGrid.d.ts +62 -0
- package/dist/components/XOnTableGrid.d.ts.map +1 -0
- package/dist/components/XOnTableGrid.js +42 -0
- package/dist/components/XOnTableHeader.d.ts +34 -0
- package/dist/components/XOnTableHeader.d.ts.map +1 -0
- package/dist/components/XOnTableHeader.js +7 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +4 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +13 -0
- package/dist/hooks/useAutoRows.d.ts +9 -0
- package/dist/hooks/useAutoRows.d.ts.map +1 -0
- package/dist/hooks/useAutoRows.js +41 -0
- package/dist/hooks/useClipboardCatcher.d.ts +15 -0
- package/dist/hooks/useClipboardCatcher.d.ts.map +1 -0
- package/dist/hooks/useClipboardCatcher.js +29 -0
- package/dist/hooks/useColumnFilters.d.ts +17 -0
- package/dist/hooks/useColumnFilters.d.ts.map +1 -0
- package/dist/hooks/useColumnFilters.js +95 -0
- package/dist/hooks/useColumnGroups.d.ts +27 -0
- package/dist/hooks/useColumnGroups.d.ts.map +1 -0
- package/dist/hooks/useColumnGroups.js +89 -0
- package/dist/hooks/useColumnResize.d.ts +12 -0
- package/dist/hooks/useColumnResize.d.ts.map +1 -0
- package/dist/hooks/useColumnResize.js +27 -0
- package/dist/hooks/useEditorOverlay.d.ts +24 -0
- package/dist/hooks/useEditorOverlay.d.ts.map +1 -0
- package/dist/hooks/useEditorOverlay.js +74 -0
- package/dist/hooks/useFillHandle.d.ts +16 -0
- package/dist/hooks/useFillHandle.d.ts.map +1 -0
- package/dist/hooks/useFillHandle.js +52 -0
- package/dist/hooks/useGridKeydown.d.ts +18 -0
- package/dist/hooks/useGridKeydown.d.ts.map +1 -0
- package/dist/hooks/useGridKeydown.js +109 -0
- package/dist/hooks/useOutsideClick.d.ts +7 -0
- package/dist/hooks/useOutsideClick.d.ts.map +1 -0
- package/dist/hooks/useOutsideClick.js +26 -0
- package/dist/hooks/useRangeSelection.d.ts +23 -0
- package/dist/hooks/useRangeSelection.d.ts.map +1 -0
- package/dist/hooks/useRangeSelection.js +48 -0
- package/dist/hooks/useSelectOptions.d.ts +13 -0
- package/dist/hooks/useSelectOptions.d.ts.map +1 -0
- package/dist/hooks/useSelectOptions.js +35 -0
- package/dist/hooks/useTableModel.d.ts +31 -0
- package/dist/hooks/useTableModel.d.ts.map +1 -0
- package/dist/hooks/useTableModel.js +135 -0
- package/dist/hooks/useValidation.d.ts +9 -0
- package/dist/hooks/useValidation.d.ts.map +1 -0
- package/dist/hooks/useValidation.js +42 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/cellKey.d.ts +2 -0
- package/dist/utils/cellKey.d.ts.map +1 -0
- package/dist/utils/cellKey.js +3 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/tsv.d.ts +3 -0
- package/dist/utils/tsv.d.ts.map +1 -0
- package/dist/utils/tsv.js +9 -0
- package/package.json +36 -0
- package/src/styles/xontable.base.css +71 -0
- package/src/styles/xontable.css +3 -0
- package/src/styles/xontable.filter.css +65 -0
- package/src/styles/xontable.theme.css +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xontable
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# xontable
|
|
2
|
+
|
|
3
|
+
A spreadsheet-like React table component with selection, clipboard, fill handle, validation, filters, select dropdowns, and checkbox cells.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install xontable
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { XOnTable, type ColumnDef } from "xontable";
|
|
15
|
+
import "xontable/styles";
|
|
16
|
+
|
|
17
|
+
const columns: ColumnDef<Row>[] = [
|
|
18
|
+
{ key: "name", label: "Name", type: "text", editable: true },
|
|
19
|
+
{ key: "qty", label: "Qty", type: "number", editable: true },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
<XOnTable columns={columns} rows={rows} onChange={setRows} />;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
- Built for React 19+
|
|
27
|
+
- Styles are included via `xontable/styles`
|
|
28
|
+
|
|
29
|
+
See the root `BOOK.md` for full developer documentation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XOnTable.d.ts","sourceRoot":"","sources":["../src/XOnTable.tsx"],"names":[],"mappings":"AACA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAK7C,wBAAgB,QAAQ,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,2CAkIlF"}
|
package/dist/XOnTable.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./styles/xontable.css";
|
|
4
|
+
import { SelectMenu, XOnTableGrid } from "./components";
|
|
5
|
+
import { useAutoRows, useClipboardCatcher, useColumnFilters, useColumnGroups, useColumnResize, useEditorOverlay, useFillHandle, useGridKeydown, useOutsideClick, useRangeSelection, useSelectOptions, useTableModel } from "./hooks";
|
|
6
|
+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
7
|
+
export function XOnTable(props) {
|
|
8
|
+
const { columns, rows, rowIdKey = "id", onChange, readOnly = false, theme = "light" } = props;
|
|
9
|
+
const activeCellRef = React.useRef(null);
|
|
10
|
+
const [editingRowId, setEditingRowId] = React.useState(null);
|
|
11
|
+
const { normalizedRows, handleChange, createRow } = useAutoRows(columns, rows, rowIdKey, onChange, editingRowId);
|
|
12
|
+
const filters = useColumnFilters(columns, normalizedRows);
|
|
13
|
+
const { visibleColumns, groupHeaders, getColWidth, setColWidth, toggleGroup, resetWidths, getOrigIndex } = useColumnGroups({ columns });
|
|
14
|
+
const { getOptions, ensureOptions, isLoading } = useSelectOptions(columns);
|
|
15
|
+
const selection = useRangeSelection();
|
|
16
|
+
const [copiedBounds, setCopiedBounds] = React.useState(null);
|
|
17
|
+
React.useEffect(() => { resetWidths(); }, [columns, resetWidths]);
|
|
18
|
+
const { data, active, setActive, getValue, updateCells, moveActive, rowCount, colCount, hasError, getError, setCellErrorView, undo, redo } = useTableModel({
|
|
19
|
+
columns: visibleColumns.map((v) => v.col),
|
|
20
|
+
rows: normalizedRows,
|
|
21
|
+
rowFilter: filters.rowFilter,
|
|
22
|
+
onChange: handleChange,
|
|
23
|
+
createRow,
|
|
24
|
+
});
|
|
25
|
+
React.useEffect(() => { if (active.c >= colCount)
|
|
26
|
+
setActive({ r: active.r, c: Math.max(0, colCount - 1) }); }, [active, colCount, setActive]);
|
|
27
|
+
const activeCol = visibleColumns[active.c]?.col;
|
|
28
|
+
const activeRow = data[active.r];
|
|
29
|
+
const validateSelect = React.useCallback((r, c, value, row) => {
|
|
30
|
+
const col = visibleColumns[c]?.col;
|
|
31
|
+
if (!col || col.type !== "select" || !row)
|
|
32
|
+
return null;
|
|
33
|
+
const opts = getOptions(row, col);
|
|
34
|
+
const match = opts.find((o) => o.value === value || o.label === value) ?? null;
|
|
35
|
+
if (!match && value.trim() !== "") {
|
|
36
|
+
setCellErrorView(r, c, "Invalid option");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
setCellErrorView(r, c, null);
|
|
40
|
+
return match;
|
|
41
|
+
}, [getOptions, setCellErrorView, visibleColumns]);
|
|
42
|
+
const normalizeSelectValue = React.useCallback((r, c, value, row) => { const match = validateSelect(r, c, value, row); return match ? match.value : value; }, [validateSelect]);
|
|
43
|
+
const ensureSelect = React.useCallback((r, c, row) => { const col = visibleColumns[c]?.col; if (!col || col.type !== "select" || !row)
|
|
44
|
+
return; ensureOptions(row, col); }, [ensureOptions, visibleColumns]);
|
|
45
|
+
const { startResize } = useColumnResize({ colCount, getWidth: getColWidth, setWidth: setColWidth, minWidth: 60 });
|
|
46
|
+
const measureRef = React.useRef(null);
|
|
47
|
+
React.useEffect(() => () => { if (measureRef.current)
|
|
48
|
+
measureRef.current.remove(); }, []);
|
|
49
|
+
const measureText = React.useCallback((text) => { let el = measureRef.current; if (!el) {
|
|
50
|
+
el = document.createElement("div");
|
|
51
|
+
el.className = "xontable-cell xontable-measure";
|
|
52
|
+
document.body.appendChild(el);
|
|
53
|
+
measureRef.current = el;
|
|
54
|
+
} el.textContent = text || ""; return Math.ceil(el.getBoundingClientRect().width); }, []);
|
|
55
|
+
const autoFitCol = React.useCallback((visibleIndex) => { const orig = getOrigIndex(visibleIndex); if (orig == null)
|
|
56
|
+
return; let max = measureText(columns[orig]?.label ?? ""); for (let r = 0; r < data.length; r++) {
|
|
57
|
+
const w = measureText(getValue(r, visibleIndex));
|
|
58
|
+
if (w > max)
|
|
59
|
+
max = w;
|
|
60
|
+
} setColWidth(visibleIndex, Math.min(600, Math.max(60, max + 15))); }, [columns, data.length, getOrigIndex, getValue, measureText, setColWidth]);
|
|
61
|
+
const { isEditing, draft, setDraft, editorRect, editorRef, startEdit, commitEdit, onEditorKeyDown } = useEditorOverlay({
|
|
62
|
+
active, activeCellRef, getValue,
|
|
63
|
+
onCommit: (value) => {
|
|
64
|
+
const row = data[active.r];
|
|
65
|
+
const col = visibleColumns[active.c]?.col;
|
|
66
|
+
const v = col?.type === "select" ? normalizeSelectValue(active.r, active.c, value, row) : value;
|
|
67
|
+
updateCells([{ r: active.r, c: active.c, value: v }], { type: "edit", cell: active });
|
|
68
|
+
},
|
|
69
|
+
onEnter: (dir) => moveActive(dir, 0), onTab: (dir) => moveActive(0, dir),
|
|
70
|
+
});
|
|
71
|
+
React.useEffect(() => { if (!isEditing)
|
|
72
|
+
setEditingRowId(null); }, [isEditing]);
|
|
73
|
+
const startEditCell = React.useCallback((initial) => { if (readOnly)
|
|
74
|
+
return; const id = activeRow ? String(activeRow[rowIdKey] ?? "") : ""; setEditingRowId(id || null); ensureSelect(active.r, active.c, activeRow); startEdit(initial); }, [active.c, active.r, activeRow, ensureSelect, readOnly, rowIdKey, startEdit]);
|
|
75
|
+
const toggleCheckbox = React.useCallback((r, c) => {
|
|
76
|
+
if (readOnly)
|
|
77
|
+
return;
|
|
78
|
+
const col = visibleColumns[c]?.col;
|
|
79
|
+
const row = data[r];
|
|
80
|
+
if (!col || col.type !== "checkbox" || !row)
|
|
81
|
+
return;
|
|
82
|
+
const checked = row[col.key] === true || row[col.key] === "true";
|
|
83
|
+
updateCells([{ r, c, value: !checked }], { type: "edit", cell: { r, c } });
|
|
84
|
+
}, [data, readOnly, updateCells, visibleColumns]);
|
|
85
|
+
const clearRange = React.useCallback(() => { const b = selection.getBounds(); if (!b) {
|
|
86
|
+
updateCells([{ r: active.r, c: active.c, value: "" }], { type: "edit", cell: active });
|
|
87
|
+
return;
|
|
88
|
+
} const updates = []; for (let r = b.r1; r <= b.r2; r++)
|
|
89
|
+
for (let c = b.c1; c <= b.c2; c++)
|
|
90
|
+
updates.push({ r, c, value: "" }); updateCells(updates, { type: "edit", cell: active }); }, [active, selection, updateCells]);
|
|
91
|
+
const { clipRef, focusClipboard, onCopy, onPaste } = useClipboardCatcher({
|
|
92
|
+
isEditing,
|
|
93
|
+
getCopyBlock: () => { const b = selection.getBounds(); if (!b)
|
|
94
|
+
return [[getValue(active.r, active.c)]]; const block = []; for (let r = b.r1; r <= b.r2; r++) {
|
|
95
|
+
const row = [];
|
|
96
|
+
for (let c = b.c1; c <= b.c2; c++)
|
|
97
|
+
row.push(getValue(r, c));
|
|
98
|
+
block.push(row);
|
|
99
|
+
} return block; },
|
|
100
|
+
onCopy: () => setCopiedBounds(selection.getBounds() ?? { r1: active.r, r2: active.r, c1: active.c, c2: active.c }),
|
|
101
|
+
onPasteBlock: (block) => { if (readOnly)
|
|
102
|
+
return; const updates = []; for (let rOff = 0; rOff < block.length; rOff++)
|
|
103
|
+
for (let cOff = 0; cOff < block[rOff].length; cOff++) {
|
|
104
|
+
const r = active.r + rOff;
|
|
105
|
+
const c = active.c + cOff;
|
|
106
|
+
if (c < colCount)
|
|
107
|
+
updates.push({ r, c, value: block[rOff][cOff] });
|
|
108
|
+
} updateCells(updates, { type: "paste", cell: active }); updates.forEach((u) => validateSelect(u.r, u.c, u.value, data[u.r])); },
|
|
109
|
+
});
|
|
110
|
+
React.useEffect(() => { if (!isEditing)
|
|
111
|
+
focusClipboard(); }, [focusClipboard, isEditing]);
|
|
112
|
+
const openSelectAt = React.useCallback((r, c) => {
|
|
113
|
+
if (readOnly)
|
|
114
|
+
return;
|
|
115
|
+
setActive({ r, c });
|
|
116
|
+
selection.startSelection({ r, c });
|
|
117
|
+
focusClipboard();
|
|
118
|
+
const row = data[r];
|
|
119
|
+
const id = row ? String(row[rowIdKey] ?? "") : "";
|
|
120
|
+
setEditingRowId(id || null);
|
|
121
|
+
ensureSelect(r, c, row);
|
|
122
|
+
requestAnimationFrame(() => startEdit());
|
|
123
|
+
}, [data, ensureSelect, focusClipboard, readOnly, rowIdKey, selection, setActive, startEdit]);
|
|
124
|
+
const { startDrag, isPreview } = useFillHandle({
|
|
125
|
+
onApply: (startR, startC, endR, endC) => {
|
|
126
|
+
if (readOnly)
|
|
127
|
+
return;
|
|
128
|
+
const value = getValue(startR, startC);
|
|
129
|
+
const updates = [];
|
|
130
|
+
const dr = Math.abs(endR - startR);
|
|
131
|
+
const dc = Math.abs(endC - startC);
|
|
132
|
+
if (dc >= dr) {
|
|
133
|
+
const from = Math.min(startC, endC);
|
|
134
|
+
const to = Math.max(startC, endC);
|
|
135
|
+
for (let c = from; c <= to; c++)
|
|
136
|
+
updates.push({ r: startR, c, value });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const from = Math.min(startR, endR);
|
|
140
|
+
const to = Math.max(startR, endR);
|
|
141
|
+
for (let r = from; r <= to; r++)
|
|
142
|
+
updates.push({ r, c: startC, value });
|
|
143
|
+
}
|
|
144
|
+
updateCells(updates, { type: "fill", cell: { r: startR, c: startC } });
|
|
145
|
+
updates.forEach((u) => validateSelect(u.r, u.c, u.value, data[u.r]));
|
|
146
|
+
focusClipboard();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const onShiftMove = React.useCallback((dr, dc) => { const next = { r: clamp(active.r + dr, 0, rowCount - 1), c: clamp(active.c + dc, 0, colCount - 1) }; if (!selection.selection)
|
|
150
|
+
selection.startSelection(active); selection.updateSelection(next); setActive(next); }, [active, colCount, rowCount, selection, setActive]);
|
|
151
|
+
const onGridKeyDown = useGridKeydown({
|
|
152
|
+
active, rowCount, colCount, isEditing, moveActive, moveTo: (r, c) => setActive({ r, c }),
|
|
153
|
+
startEdit: readOnly ? () => { } : startEditCell,
|
|
154
|
+
clearCell: () => { if (!readOnly)
|
|
155
|
+
clearRange(); },
|
|
156
|
+
undo: () => { if (!readOnly)
|
|
157
|
+
undo(); }, redo: () => { if (!readOnly)
|
|
158
|
+
redo(); },
|
|
159
|
+
onShiftMove,
|
|
160
|
+
});
|
|
161
|
+
const onGridKeyDownWithCopy = React.useCallback((e) => {
|
|
162
|
+
const target = e.target;
|
|
163
|
+
const isClip = Boolean(target && target.classList.contains("xontable-clip"));
|
|
164
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA") && !isClip)
|
|
165
|
+
return;
|
|
166
|
+
const isCopy = (e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C");
|
|
167
|
+
if (isCopy)
|
|
168
|
+
setCopiedBounds(selection.getBounds() ?? { r1: active.r, r2: active.r, c1: active.c, c2: active.c });
|
|
169
|
+
else
|
|
170
|
+
setCopiedBounds(null);
|
|
171
|
+
onGridKeyDown(e);
|
|
172
|
+
}, [active.c, active.r, onGridKeyDown, selection]);
|
|
173
|
+
useOutsideClick({ isOpen: filters.filterOpenKey != null, onClose: filters.closeFilter });
|
|
174
|
+
return (_jsxs("div", { className: `xontable-wrap theme-${theme}${readOnly ? " is-readonly" : ""}`, children: [_jsx("textarea", { ref: clipRef, className: "xontable-clip", name: "xontable-clip", "aria-hidden": "true", tabIndex: -1, onCopy: onCopy, onPaste: onPaste, onKeyDown: onGridKeyDownWithCopy, readOnly: true }), _jsx("div", { className: "xontable-surface", tabIndex: 0, onFocus: (e) => {
|
|
175
|
+
const target = e.target;
|
|
176
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA"))
|
|
177
|
+
return;
|
|
178
|
+
focusClipboard();
|
|
179
|
+
}, onKeyDown: onGridKeyDownWithCopy, children: _jsx(XOnTableGrid, { columns: visibleColumns, groups: groupHeaders.some((g) => g.label) ? groupHeaders : undefined, rowNumberWidth: 44, data: data, rowIdKey: rowIdKey, active: active, activeCol: active.c, isEditing: isEditing, readOnly: readOnly, selectionBounds: selection.getBounds(), copiedBounds: copiedBounds, getColWidth: getColWidth, getValue: getValue, hasError: hasError, getError: getError, isPreview: isPreview, activeCellRef: activeCellRef, onCellMouseDown: (r, c, ev) => { ev.preventDefault(); setActive({ r, c }); selection.startSelection({ r, c }); setCopiedBounds(null); focusClipboard(); if (filters.filterOpenKey)
|
|
180
|
+
filters.closeFilter(); }, onCellMouseEnter: (r, c) => { if (selection.isSelecting)
|
|
181
|
+
selection.updateSelection({ r, c }); }, onCellDoubleClick: (r, c) => { setActive({ r, c }); startEditCell(); }, onCheckboxToggle: toggleCheckbox, onSelectOpen: openSelectAt, onFillStart: (r, c, ev) => { ev.preventDefault(); ev.stopPropagation(); startDrag(r, c); }, onResizeStart: (c, ev) => { ev.preventDefault(); ev.stopPropagation(); startResize(c, ev.clientX); }, onResizeDoubleClick: (c, ev) => { ev.preventDefault(); ev.stopPropagation(); autoFitCol(c); }, onGroupToggle: toggleGroup, filterOpenKey: filters.filterOpenKey, filterSearch: filters.filterSearch, getFilterOptions: filters.getFilterOptions, isFilterChecked: filters.isFilterChecked, isFilterAllChecked: filters.isAllChecked, onFilterOpen: filters.openFilter, onFilterSearch: filters.setFilterSearch, onFilterToggle: filters.toggleFilterValue, onFilterToggleAll: filters.toggleAll }) }), isEditing && editorRect && (_jsx("input", { ref: editorRef, className: "xontable-editor", name: "xontable-editor", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onEditorKeyDown, onBlur: () => { commitEdit(); validateSelect(active.r, active.c, draft, activeRow); }, style: { position: "fixed", left: editorRect.left, top: editorRect.top, width: editorRect.width, height: editorRect.height } })), _jsx(SelectMenu, { isOpen: Boolean(isEditing && activeCol?.type === "select"), rect: editorRect, options: activeRow && activeCol ? getOptions(activeRow, activeCol) : [], loading: Boolean(activeRow && activeCol && isLoading(activeRow, activeCol)), filter: draft, onSelect: (v) => { setDraft(v); commitEdit(v); validateSelect(active.r, active.c, v, activeRow); } })] }));
|
|
182
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type ColumnFilterMenuProps = {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
search: string;
|
|
4
|
+
options: string[];
|
|
5
|
+
allChecked: boolean;
|
|
6
|
+
isChecked: (value: string) => boolean;
|
|
7
|
+
onSearchChange: (value: string) => void;
|
|
8
|
+
onToggle: (value: string) => void;
|
|
9
|
+
onToggleAll: () => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function ColumnFilterMenu(props: ColumnFilterMenuProps): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=ColumnFilterMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ColumnFilterMenu.d.ts","sourceRoot":"","sources":["../../src/components/ColumnFilterMenu.tsx"],"names":[],"mappings":"AAEA,KAAK,qBAAqB,GAAG;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,kDAiD5D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function ColumnFilterMenu(props) {
|
|
3
|
+
const { isOpen, search, options, allChecked, isChecked, onSearchChange, onToggle, onToggleAll, } = props;
|
|
4
|
+
if (!isOpen)
|
|
5
|
+
return null;
|
|
6
|
+
return (_jsxs("div", { className: "xontable-filter-menu", children: [_jsx("input", { className: "xontable-filter-search", name: "xontable-filter-search", value: search, onChange: (e) => onSearchChange(e.target.value), onKeyDown: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), placeholder: "Search" }), _jsx("div", { className: "xontable-filter-actions", children: _jsxs("label", { className: "xontable-filter-toggle", children: [_jsx("input", { type: "checkbox", checked: allChecked, onChange: onToggleAll }), _jsx("span", { children: "All" })] }) }), _jsx("div", { className: "xontable-filter-list", children: options.map((opt) => (_jsxs("label", { className: "xontable-filter-item", children: [_jsx("input", { type: "checkbox", checked: isChecked(opt), onChange: () => onToggle(opt) }), _jsx("span", { title: opt, children: opt || "(blank)" })] }, opt))) })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Option = {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
};
|
|
5
|
+
type SelectMenuProps = {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
rect: DOMRect | null;
|
|
8
|
+
options: Option[];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
filter: string;
|
|
11
|
+
onSelect: (value: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function SelectMenu(props: SelectMenuProps): import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=SelectMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectMenu.d.ts","sourceRoot":"","sources":["../../src/components/SelectMenu.tsx"],"names":[],"mappings":"AAEA,KAAK,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,kDA+DhD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function SelectMenu(props) {
|
|
3
|
+
const { isOpen, rect, options, loading, filter, onSelect } = props;
|
|
4
|
+
if (!isOpen || !rect)
|
|
5
|
+
return null;
|
|
6
|
+
const q = filter.toLowerCase();
|
|
7
|
+
const list = q ? options.filter((o) => o.label.toLowerCase().includes(q)) : options;
|
|
8
|
+
const current = filter.trim();
|
|
9
|
+
const vw = window.innerWidth;
|
|
10
|
+
const vh = window.innerHeight;
|
|
11
|
+
const width = Math.min(rect.width, vw - 16);
|
|
12
|
+
const left = Math.min(Math.max(8, rect.left), vw - width - 8);
|
|
13
|
+
const maxH = Math.min(220, vh - rect.bottom - 12);
|
|
14
|
+
const placeAbove = maxH < 120;
|
|
15
|
+
const top = placeAbove ? Math.max(8, rect.top - 6) : rect.bottom + 2;
|
|
16
|
+
const renderLabel = (label) => {
|
|
17
|
+
if (!q)
|
|
18
|
+
return label;
|
|
19
|
+
const idx = label.toLowerCase().indexOf(q);
|
|
20
|
+
if (idx < 0)
|
|
21
|
+
return label;
|
|
22
|
+
const before = label.slice(0, idx);
|
|
23
|
+
const match = label.slice(idx, idx + q.length);
|
|
24
|
+
const after = label.slice(idx + q.length);
|
|
25
|
+
return (_jsxs(_Fragment, { children: [before, _jsx("strong", { children: match }), after] }));
|
|
26
|
+
};
|
|
27
|
+
return (_jsxs("div", { className: "xontable-select-menu", style: {
|
|
28
|
+
position: "fixed",
|
|
29
|
+
left,
|
|
30
|
+
top,
|
|
31
|
+
width,
|
|
32
|
+
maxHeight: placeAbove ? Math.min(220, rect.top - 12) : maxH,
|
|
33
|
+
transform: placeAbove ? "translateY(-100%)" : "none",
|
|
34
|
+
}, children: [loading && _jsx("div", { className: "xontable-select-item", children: "Loading..." }), !loading && list.length === 0 && (_jsx("div", { className: "xontable-select-item", children: "No results" })), !loading && list.map((opt) => (_jsx("button", { type: "button", className: [
|
|
35
|
+
"xontable-select-item",
|
|
36
|
+
current && (opt.value === current || opt.label === current) ? "is-active" : "",
|
|
37
|
+
].join(" "), onMouseDown: (e) => e.preventDefault(), onClick: () => onSelect(opt.value), title: opt.label, children: renderLabel(opt.label) }, opt.value)))] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { CellPos, ColumnDef } from "../types";
|
|
3
|
+
type GroupHeader = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
width: number;
|
|
7
|
+
collapsible: boolean;
|
|
8
|
+
collapsed: boolean;
|
|
9
|
+
};
|
|
10
|
+
type GridProps<Row extends Record<string, any>> = {
|
|
11
|
+
columns: Array<{
|
|
12
|
+
col: ColumnDef<Row>;
|
|
13
|
+
idx: number | null;
|
|
14
|
+
}>;
|
|
15
|
+
groups?: GroupHeader[];
|
|
16
|
+
rowNumberWidth: number;
|
|
17
|
+
data: Row[];
|
|
18
|
+
rowIdKey: keyof Row;
|
|
19
|
+
active: CellPos;
|
|
20
|
+
activeCol: number;
|
|
21
|
+
isEditing: boolean;
|
|
22
|
+
readOnly: boolean;
|
|
23
|
+
selectionBounds: {
|
|
24
|
+
r1: number;
|
|
25
|
+
r2: number;
|
|
26
|
+
c1: number;
|
|
27
|
+
c2: number;
|
|
28
|
+
} | null;
|
|
29
|
+
copiedBounds: {
|
|
30
|
+
r1: number;
|
|
31
|
+
r2: number;
|
|
32
|
+
c1: number;
|
|
33
|
+
c2: number;
|
|
34
|
+
} | null;
|
|
35
|
+
getColWidth: (c: number) => number;
|
|
36
|
+
getValue: (r: number, c: number) => string;
|
|
37
|
+
hasError: (r: number, c: number) => boolean;
|
|
38
|
+
getError: (r: number, c: number) => string | null;
|
|
39
|
+
isPreview: (r: number, c: number) => boolean;
|
|
40
|
+
activeCellRef: React.RefObject<HTMLDivElement | null>;
|
|
41
|
+
onCellMouseDown: (r: number, c: number, ev: React.MouseEvent) => void;
|
|
42
|
+
onCellMouseEnter: (r: number, c: number, ev: React.MouseEvent) => void;
|
|
43
|
+
onCellDoubleClick: (r: number, c: number) => void;
|
|
44
|
+
onCheckboxToggle: (r: number, c: number) => void;
|
|
45
|
+
onSelectOpen: (r: number, c: number) => void;
|
|
46
|
+
onFillStart: (r: number, c: number, ev: React.MouseEvent) => void;
|
|
47
|
+
onResizeStart: (c: number, ev: React.MouseEvent) => void;
|
|
48
|
+
onResizeDoubleClick: (c: number, ev: React.MouseEvent) => void;
|
|
49
|
+
onGroupToggle: (key: string) => void;
|
|
50
|
+
filterOpenKey: string | null;
|
|
51
|
+
filterSearch: string;
|
|
52
|
+
getFilterOptions: (key: string) => string[];
|
|
53
|
+
isFilterChecked: (key: string, value: string) => boolean;
|
|
54
|
+
isFilterAllChecked: (key: string) => boolean;
|
|
55
|
+
onFilterOpen: (key: string) => void;
|
|
56
|
+
onFilterSearch: (value: string) => void;
|
|
57
|
+
onFilterToggle: (key: string, value: string) => void;
|
|
58
|
+
onFilterToggleAll: (key: string) => void;
|
|
59
|
+
};
|
|
60
|
+
export declare function XOnTableGrid<Row extends Record<string, any>>(props: GridProps<Row>): import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=XOnTableGrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XOnTableGrid.d.ts","sourceRoot":"","sources":["../../src/components/XOnTableGrid.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { XOnTableHeader } from "./XOnTableHeader";
|
|
3
|
+
export function XOnTableGrid(props) {
|
|
4
|
+
const { columns, data, rowIdKey, active, isEditing, readOnly, selectionBounds, copiedBounds, getColWidth, getValue, hasError, getError, isPreview, activeCellRef, onCellMouseDown, onCellMouseEnter, onCellDoubleClick, onCheckboxToggle, onSelectOpen, onFillStart } = props;
|
|
5
|
+
return (_jsxs("div", { className: "xontable", children: [_jsx(XOnTableHeader, { ...props }), data.map((row, r) => (_jsxs("div", { className: ["xontable-row", r % 2 === 1 ? "is-zebra" : ""].join(" "), "data-row": r, children: [_jsx("div", { className: ["xontable-cell", "xontable-rownum-cell", r === active.r ? "is-active-rownum" : ""].join(" "), style: { width: props.rowNumberWidth }, children: r + 1 }), columns.map(({ col, idx }, c) => {
|
|
6
|
+
const isActive = active.r === r && active.c === c;
|
|
7
|
+
const invalid = hasError(r, c);
|
|
8
|
+
const preview = isPreview(r, c);
|
|
9
|
+
const inSel = !!selectionBounds && r >= selectionBounds.r1 && r <= selectionBounds.r2 && c >= selectionBounds.c1 && c <= selectionBounds.c2;
|
|
10
|
+
const inCopy = !!copiedBounds && r >= copiedBounds.r1 && r <= copiedBounds.r2 && c >= copiedBounds.c1 && c <= copiedBounds.c2;
|
|
11
|
+
const selTop = inSel && selectionBounds && r === selectionBounds.r1;
|
|
12
|
+
const selBottom = inSel && selectionBounds && r === selectionBounds.r2;
|
|
13
|
+
const selLeft = inSel && selectionBounds && c === selectionBounds.c1;
|
|
14
|
+
const selRight = inSel && selectionBounds && c === selectionBounds.c2;
|
|
15
|
+
const copyTop = inCopy && copiedBounds && r === copiedBounds.r1;
|
|
16
|
+
const copyBottom = inCopy && copiedBounds && r === copiedBounds.r2;
|
|
17
|
+
const copyLeft = inCopy && copiedBounds && c === copiedBounds.c1;
|
|
18
|
+
const copyRight = inCopy && copiedBounds && c === copiedBounds.c2;
|
|
19
|
+
const isCheckbox = col.type === "checkbox";
|
|
20
|
+
const isSelect = col.type === "select";
|
|
21
|
+
const checked = row[col.key] === true || row[col.key] === "true";
|
|
22
|
+
const isPlaceholder = idx == null;
|
|
23
|
+
return (_jsxs("div", { ref: isActive ? activeCellRef : null, "data-row": r, "data-col": c, className: [
|
|
24
|
+
"xontable-cell",
|
|
25
|
+
isActive ? "is-active" : "",
|
|
26
|
+
inSel ? "is-range" : "",
|
|
27
|
+
selTop ? "is-range-top" : "",
|
|
28
|
+
selRight ? "is-range-right" : "",
|
|
29
|
+
selBottom ? "is-range-bottom" : "",
|
|
30
|
+
selLeft ? "is-range-left" : "",
|
|
31
|
+
inCopy ? "is-copied-range" : "",
|
|
32
|
+
copyTop ? "is-copied-top" : "",
|
|
33
|
+
copyRight ? "is-copied-right" : "",
|
|
34
|
+
copyBottom ? "is-copied-bottom" : "",
|
|
35
|
+
copyLeft ? "is-copied-left" : "",
|
|
36
|
+
isCheckbox ? "is-checkbox" : "",
|
|
37
|
+
isSelect ? "is-select" : "",
|
|
38
|
+
invalid ? "is-invalid" : "",
|
|
39
|
+
preview ? "is-fill-preview" : "",
|
|
40
|
+
].join(" "), style: { width: getColWidth(c) }, title: invalid ? (getError(r, c) ?? "") : "", onMouseDown: (ev) => onCellMouseDown(r, c, ev), onMouseEnter: (ev) => onCellMouseEnter(r, c, ev), onDoubleClick: () => onCellDoubleClick(r, c), children: [isCheckbox ? (_jsx("input", { type: "checkbox", className: "xontable-checkbox", name: "xontable-checkbox", checked: checked, onChange: () => onCheckboxToggle(r, c), onClick: (ev) => ev.stopPropagation(), onMouseDown: (ev) => ev.stopPropagation() })) : (isPlaceholder ? "" : getValue(r, c)), isSelect && !readOnly && !isEditing && (_jsx("button", { type: "button", className: "xontable-select-trigger", title: "Open", onClick: (ev) => { ev.preventDefault(); ev.stopPropagation(); onSelectOpen(r, c); } })), isActive && !isEditing && !readOnly && (_jsx("div", { className: "xontable-fill-handle", onMouseDown: (ev) => onFillStart(r, c, ev), title: "Drag to fill" }))] }, col.key + String(idx ?? c)));
|
|
41
|
+
})] }, String(row[rowIdKey] ?? r))))] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ColumnDef } from "../types";
|
|
3
|
+
type GroupHeader = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
width: number;
|
|
7
|
+
collapsible: boolean;
|
|
8
|
+
collapsed: boolean;
|
|
9
|
+
};
|
|
10
|
+
type HeaderProps<Row extends Record<string, any>> = {
|
|
11
|
+
columns: Array<{
|
|
12
|
+
col: ColumnDef<Row>;
|
|
13
|
+
idx: number | null;
|
|
14
|
+
}>;
|
|
15
|
+
groups?: GroupHeader[];
|
|
16
|
+
rowNumberWidth: number;
|
|
17
|
+
activeCol: number;
|
|
18
|
+
getColWidth: (c: number) => number;
|
|
19
|
+
onResizeStart: (c: number, ev: React.MouseEvent) => void;
|
|
20
|
+
onResizeDoubleClick: (c: number, ev: React.MouseEvent) => void;
|
|
21
|
+
onGroupToggle: (key: string) => void;
|
|
22
|
+
filterOpenKey: string | null;
|
|
23
|
+
filterSearch: string;
|
|
24
|
+
getFilterOptions: (key: string) => string[];
|
|
25
|
+
isFilterChecked: (key: string, value: string) => boolean;
|
|
26
|
+
isFilterAllChecked: (key: string) => boolean;
|
|
27
|
+
onFilterOpen: (key: string) => void;
|
|
28
|
+
onFilterSearch: (value: string) => void;
|
|
29
|
+
onFilterToggle: (key: string, value: string) => void;
|
|
30
|
+
onFilterToggleAll: (key: string) => void;
|
|
31
|
+
};
|
|
32
|
+
export declare function XOnTableHeader<Row extends Record<string, any>>(props: HeaderProps<Row>): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=XOnTableHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XOnTableHeader.d.ts","sourceRoot":"","sources":["../../src/components/XOnTableHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C,KAAK,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3G,KAAK,WAAW,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAClD,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACzD,mBAAmB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACzD,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,CAAC;AAEF,wBAAgB,cAAc,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,GAAG,CAAC,2CAiDtF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Filter, Minus, Plus } from "lucide-react";
|
|
3
|
+
import { ColumnFilterMenu } from "./ColumnFilterMenu";
|
|
4
|
+
export function XOnTableHeader(props) {
|
|
5
|
+
const { columns, groups, rowNumberWidth, activeCol, getColWidth, onResizeStart, onResizeDoubleClick, onGroupToggle, filterOpenKey, filterSearch, getFilterOptions, isFilterChecked, isFilterAllChecked, onFilterOpen, onFilterSearch, onFilterToggle, onFilterToggleAll } = props;
|
|
6
|
+
return (_jsxs(_Fragment, { children: [groups && groups.length > 0 && (_jsxs("div", { className: "xontable-row xontable-group-row", children: [_jsx("div", { className: "xontable-cell xontable-rownum-cell xontable-group-cell", style: { width: rowNumberWidth } }), groups.map((g) => (_jsxs("div", { className: "xontable-cell xontable-group-cell", style: { width: g.width }, children: [_jsx("span", { className: "xontable-group-label", children: g.label }), g.collapsible && (_jsx("button", { type: "button", className: "xontable-group-toggle", onClick: () => onGroupToggle(g.key), title: g.collapsed ? "Expand" : "Collapse", children: g.collapsed ? _jsx(Plus, { size: 12 }) : _jsx(Minus, { size: 12 }) }))] }, g.key)))] })), _jsxs("div", { className: "xontable-row xontable-head", children: [_jsx("div", { className: "xontable-cell xontable-rownum-cell xontable-head-cell", style: { width: rowNumberWidth } }), columns.map(({ col, idx }, c) => (_jsxs("div", { className: ["xontable-cell", "xontable-head-cell", c === activeCol ? "is-active-col-head" : ""].join(" "), style: { width: getColWidth(c) }, children: [_jsx("span", { className: "xontable-head-label", children: col.label }), idx != null && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "xontable-filter-btn", onClick: () => onFilterOpen(col.key), title: "Filter", children: _jsx(Filter, { size: 14 }) }), _jsx("div", { className: "xontable-col-resizer", onMouseDown: (ev) => onResizeStart(c, ev), onDoubleClick: (ev) => onResizeDoubleClick(c, ev), title: "Drag to resize" }), _jsx(ColumnFilterMenu, { isOpen: filterOpenKey === col.key, search: filterSearch, options: getFilterOptions(col.key), allChecked: isFilterAllChecked(col.key), isChecked: (v) => isFilterChecked(col.key, v), onSearchChange: onFilterSearch, onToggle: (v) => onFilterToggle(col.key, v), onToggleAll: () => onFilterToggleAll(col.key) })] }))] }, col.key + String(idx ?? c))))] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { useAutoRows } from "./useAutoRows";
|
|
2
|
+
export { useClipboardCatcher } from "./useClipboardCatcher";
|
|
3
|
+
export { useColumnFilters } from "./useColumnFilters";
|
|
4
|
+
export { useColumnGroups } from "./useColumnGroups";
|
|
5
|
+
export { useColumnResize } from "./useColumnResize";
|
|
6
|
+
export { useEditorOverlay } from "./useEditorOverlay";
|
|
7
|
+
export { useFillHandle } from "./useFillHandle";
|
|
8
|
+
export { useGridKeydown } from "./useGridKeydown";
|
|
9
|
+
export { useOutsideClick } from "./useOutsideClick";
|
|
10
|
+
export { useRangeSelection } from "./useRangeSelection";
|
|
11
|
+
export { useSelectOptions } from "./useSelectOptions";
|
|
12
|
+
export { useTableModel } from "./useTableModel";
|
|
13
|
+
export { useValidation } from "./useValidation";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { useAutoRows } from "./useAutoRows";
|
|
2
|
+
export { useClipboardCatcher } from "./useClipboardCatcher";
|
|
3
|
+
export { useColumnFilters } from "./useColumnFilters";
|
|
4
|
+
export { useColumnGroups } from "./useColumnGroups";
|
|
5
|
+
export { useColumnResize } from "./useColumnResize";
|
|
6
|
+
export { useEditorOverlay } from "./useEditorOverlay";
|
|
7
|
+
export { useFillHandle } from "./useFillHandle";
|
|
8
|
+
export { useGridKeydown } from "./useGridKeydown";
|
|
9
|
+
export { useOutsideClick } from "./useOutsideClick";
|
|
10
|
+
export { useRangeSelection } from "./useRangeSelection";
|
|
11
|
+
export { useSelectOptions } from "./useSelectOptions";
|
|
12
|
+
export { useTableModel } from "./useTableModel";
|
|
13
|
+
export { useValidation } from "./useValidation";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ColumnDef, XOnTableMeta } from "../types";
|
|
2
|
+
type AutoRowsResult<Row> = {
|
|
3
|
+
normalizedRows: Row[];
|
|
4
|
+
handleChange: (rows: Row[], meta: XOnTableMeta) => void;
|
|
5
|
+
createRow: () => Row;
|
|
6
|
+
};
|
|
7
|
+
export declare function useAutoRows<Row extends Record<string, any>>(columns: ColumnDef<Row>[], rows: Row[], rowIdKey: keyof Row, onChange?: (rows: Row[], meta: XOnTableMeta) => void, pinnedRowId?: string | null): AutoRowsResult<Row>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=useAutoRows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAutoRows.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoRows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExD,KAAK,cAAc,CAAC,GAAG,IAAI;IACzB,cAAc,EAAE,GAAG,EAAE,CAAC;IACtB,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxD,SAAS,EAAE,MAAM,GAAG,CAAC;CACtB,CAAC;AAEF,wBAAgB,WAAW,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzD,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,EACzB,IAAI,EAAE,GAAG,EAAE,EACX,QAAQ,EAAE,MAAM,GAAG,EACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,EACpD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,cAAc,CAAC,GAAG,CAAC,CAiCrB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
export function useAutoRows(columns, rows, rowIdKey, onChange, pinnedRowId) {
|
|
3
|
+
const idRef = useRef(0);
|
|
4
|
+
const makeRowId = useCallback(() => `row_${Date.now()}_${idRef.current++}`, []);
|
|
5
|
+
const createRow = useCallback(() => {
|
|
6
|
+
const row = {};
|
|
7
|
+
columns.forEach((col) => { if (col.key !== String(rowIdKey))
|
|
8
|
+
row[col.key] = col.type === "checkbox" ? false : ""; });
|
|
9
|
+
row[String(rowIdKey)] = makeRowId();
|
|
10
|
+
return row;
|
|
11
|
+
}, [columns, makeRowId, rowIdKey]);
|
|
12
|
+
const isRowEmpty = useCallback((row) => {
|
|
13
|
+
const id = String(row[rowIdKey] ?? "");
|
|
14
|
+
if (pinnedRowId && id === pinnedRowId)
|
|
15
|
+
return false;
|
|
16
|
+
return columns.every((col) => {
|
|
17
|
+
if (col.key === String(rowIdKey))
|
|
18
|
+
return true;
|
|
19
|
+
const v = row[col.key];
|
|
20
|
+
if (col.type === "checkbox")
|
|
21
|
+
return v !== true;
|
|
22
|
+
return v == null || v === "";
|
|
23
|
+
});
|
|
24
|
+
}, [columns, pinnedRowId, rowIdKey]);
|
|
25
|
+
const ensureTrailingBlank = useCallback((list) => {
|
|
26
|
+
if (list.length === 0)
|
|
27
|
+
return [createRow()];
|
|
28
|
+
let lastNonEmpty = -1;
|
|
29
|
+
for (let i = 0; i < list.length; i++)
|
|
30
|
+
if (!isRowEmpty(list[i]))
|
|
31
|
+
lastNonEmpty = i;
|
|
32
|
+
const keep = lastNonEmpty === -1 ? [] : list.slice(0, lastNonEmpty + 1);
|
|
33
|
+
const tail = list[lastNonEmpty + 1];
|
|
34
|
+
return [...keep, tail && isRowEmpty(tail) ? tail : createRow()];
|
|
35
|
+
}, [createRow, isRowEmpty]);
|
|
36
|
+
const normalizedRows = useMemo(() => ensureTrailingBlank(rows), [ensureTrailingBlank, rows]);
|
|
37
|
+
const handleChange = useCallback((next, meta) => {
|
|
38
|
+
onChange?.(ensureTrailingBlank(next), meta);
|
|
39
|
+
}, [ensureTrailingBlank, onChange]);
|
|
40
|
+
return { normalizedRows, handleChange, createRow };
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClipboardEvent } from "react";
|
|
2
|
+
type ClipboardCatcherOptions = {
|
|
3
|
+
isEditing: boolean;
|
|
4
|
+
getCopyBlock: () => string[][];
|
|
5
|
+
onPasteBlock: (block: string[][]) => void;
|
|
6
|
+
onCopy?: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function useClipboardCatcher(options: ClipboardCatcherOptions): {
|
|
9
|
+
clipRef: import("react").RefObject<HTMLTextAreaElement | null>;
|
|
10
|
+
focusClipboard: () => void;
|
|
11
|
+
onCopy: (e: ClipboardEvent) => void;
|
|
12
|
+
onPaste: (e: ClipboardEvent) => void;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=useClipboardCatcher.d.ts.map
|