strike-fw-datagrid 0.1.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/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { DataGrid } from './data-grid.js';
2
+ export { rowsToCsv } from './lib/grid-csv.js';
3
+ export { cls } from 'strike-fw/ui';
@@ -0,0 +1,16 @@
1
+ import { useState } from 'strike-fw/hooks';
2
+
3
+ /** Controlled or uncontrolled value with onChange. */
4
+ export function useControllable(controlled, defaultValue, onChange) {
5
+ const isControlled = controlled !== undefined;
6
+ const [inner, setInner] = useState(
7
+ isControlled ? controlled : defaultValue
8
+ );
9
+ const value = isControlled ? controlled : inner;
10
+ function setValue(next) {
11
+ const resolved = typeof next === 'function' ? next(value) : next;
12
+ if (!isControlled) setInner(resolved);
13
+ if (onChange) onChange(resolved);
14
+ }
15
+ return [value, setValue];
16
+ }
@@ -0,0 +1,67 @@
1
+ /** Normalize DataGrid column aliases to a single shape. */
2
+ export function normalizeColumn(col) {
3
+ if (!col || typeof col !== 'object') {
4
+ throw new Error('column required');
5
+ }
6
+ const field = col.field != null ? col.field : col.key;
7
+ if (field == null || field === '') {
8
+ throw new Error('column field or key required');
9
+ }
10
+ const headerName =
11
+ col.headerName !== undefined ? col.headerName : col.label;
12
+ const renderCell =
13
+ col.renderCell !== undefined ? col.renderCell : col.render;
14
+ return {
15
+ ...col,
16
+ field: String(field),
17
+ headerName,
18
+ renderCell,
19
+ type: col.type || 'string',
20
+ sortable: col.sortable !== false,
21
+ filterable: col.filterable !== false,
22
+ editable: !!col.editable
23
+ };
24
+ }
25
+
26
+ export function getCellValue(row, col) {
27
+ if (col.valueGetter) return col.valueGetter(row);
28
+ return row[col.field];
29
+ }
30
+
31
+ export function formatCell(row, col) {
32
+ const value = getCellValue(row, col);
33
+ if (col.valueFormatter) return col.valueFormatter(value, row);
34
+ if (col.type === 'boolean') return value ? 'Yes' : 'No';
35
+ if (col.type === 'date' && value != null && value !== '') {
36
+ const d = value instanceof Date ? value : new Date(value);
37
+ if (!Number.isNaN(d.getTime())) return d.toLocaleDateString();
38
+ }
39
+ if (value == null) return '';
40
+ return String(value);
41
+ }
42
+
43
+ export function defaultAlign(type) {
44
+ return type === 'number' ? 'right' : 'left';
45
+ }
46
+
47
+ /** Derived-only columns need valueSetter to be editable. */
48
+ export function isColumnEditable(col) {
49
+ if (!col.editable) return false;
50
+ if (col.valueGetter && !col.valueSetter) return false;
51
+ return true;
52
+ }
53
+
54
+ /** Clamp a pixel width using column minWidth / maxWidth (defaults 50 / none). */
55
+ export function clampColumnWidth(col, px) {
56
+ const n = Number(px);
57
+ const width = Number.isFinite(n) ? n : 50;
58
+ const min =
59
+ col && col.minWidth != null && Number.isFinite(Number(col.minWidth))
60
+ ? Number(col.minWidth)
61
+ : 50;
62
+ const max =
63
+ col && col.maxWidth != null && Number.isFinite(Number(col.maxWidth))
64
+ ? Number(col.maxWidth)
65
+ : Infinity;
66
+ return Math.max(min, Math.min(max, Math.round(width)));
67
+ }
@@ -0,0 +1,24 @@
1
+ import { getCellValue } from './grid-columns.js';
2
+
3
+ function normalizeCol(col) {
4
+ if (typeof col === 'string') return { field: col, headerName: col };
5
+ return col;
6
+ }
7
+
8
+ function escapeCsvField(value) {
9
+ const s = value == null ? '' : String(value);
10
+ if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
11
+ return s;
12
+ }
13
+
14
+ export function rowsToCsv(rows, columns) {
15
+ const cols = columns.map(normalizeCol);
16
+ const header = cols
17
+ .map(c => escapeCsvField(c.headerName != null ? c.headerName : c.field))
18
+ .join(',');
19
+ const lines = [header];
20
+ for (const row of rows) {
21
+ lines.push(cols.map(c => escapeCsvField(getCellValue(row, c))).join(','));
22
+ }
23
+ return lines.join('\n');
24
+ }
@@ -0,0 +1,18 @@
1
+ export function parseByType(type, raw) {
2
+ if (type === 'number') {
3
+ if (raw === '' || raw == null) return null;
4
+ const n = typeof raw === 'number' ? raw : Number(raw);
5
+ return Number.isFinite(n) ? n : null;
6
+ }
7
+ if (type === 'boolean') return !!raw;
8
+ if (type === 'date') {
9
+ if (raw == null || raw === '') return null;
10
+ return raw;
11
+ }
12
+ return raw == null ? '' : String(raw);
13
+ }
14
+
15
+ export function buildUpdatedRow(oldRow, field, value, col) {
16
+ if (col && col.valueSetter) return col.valueSetter(value, oldRow);
17
+ return { ...oldRow, [field]: value };
18
+ }
@@ -0,0 +1,81 @@
1
+ import { getCellValue, formatCell } from './grid-columns.js';
2
+
3
+ export function applyQuickFilter(rows, columns, query) {
4
+ const q = query == null ? '' : String(query).trim().toLowerCase();
5
+ if (!q) return rows;
6
+ const cols = columns.filter(c => c.filterable !== false);
7
+ return rows.filter(row =>
8
+ cols.some(col => {
9
+ const text = String(formatCell(row, col)).toLowerCase();
10
+ return text.includes(q);
11
+ })
12
+ );
13
+ }
14
+
15
+ function toNumber(v) {
16
+ const n = Number(v);
17
+ return Number.isFinite(n) ? n : NaN;
18
+ }
19
+
20
+ function toTime(v) {
21
+ const d = v instanceof Date ? v : new Date(v);
22
+ const t = d.getTime();
23
+ return Number.isNaN(t) ? NaN : t;
24
+ }
25
+
26
+ function compareOrdered(cellValue, filterValue, type) {
27
+ if (type === 'number') {
28
+ return toNumber(cellValue) - toNumber(filterValue);
29
+ }
30
+ if (type === 'date') {
31
+ return toTime(cellValue) - toTime(filterValue);
32
+ }
33
+ return String(cellValue ?? '').localeCompare(String(filterValue ?? ''));
34
+ }
35
+
36
+ function equalsValue(cellValue, filterValue, type) {
37
+ if (type === 'number') {
38
+ return toNumber(cellValue) === toNumber(filterValue);
39
+ }
40
+ if (type === 'date') {
41
+ return toTime(cellValue) === toTime(filterValue);
42
+ }
43
+ if (type === 'boolean') {
44
+ return !!cellValue === !!filterValue;
45
+ }
46
+ return String(cellValue ?? '') === String(filterValue ?? '');
47
+ }
48
+
49
+ function matchFilter(cellValue, item, type) {
50
+ const op = item.operator;
51
+ const fv = item.value;
52
+ if (op === 'contains') {
53
+ return String(cellValue ?? '')
54
+ .toLowerCase()
55
+ .includes(String(fv ?? '').toLowerCase());
56
+ }
57
+ if (op === 'startsWith') {
58
+ return String(cellValue ?? '')
59
+ .toLowerCase()
60
+ .startsWith(String(fv ?? '').toLowerCase());
61
+ }
62
+ if (op === 'equals') return equalsValue(cellValue, fv, type);
63
+ if (op === '>') return compareOrdered(cellValue, fv, type) > 0;
64
+ if (op === '>=') return compareOrdered(cellValue, fv, type) >= 0;
65
+ if (op === '<') return compareOrdered(cellValue, fv, type) < 0;
66
+ if (op === '<=') return compareOrdered(cellValue, fv, type) <= 0;
67
+ return true;
68
+ }
69
+
70
+ export function applyFilterModel(rows, columns, filterModel) {
71
+ const items = filterModel && filterModel.items;
72
+ if (!items || !items.length) return rows;
73
+ return rows.filter(row =>
74
+ items.every(item => {
75
+ if (!item || !item.field) return true;
76
+ const col = columns.find(c => c.field === item.field);
77
+ if (!col) return true;
78
+ return matchFilter(getCellValue(row, col), item, col.type);
79
+ })
80
+ );
81
+ }
@@ -0,0 +1,33 @@
1
+ function clamp(n, min, max) {
2
+ return Math.max(min, Math.min(n, max));
3
+ }
4
+
5
+ export function moveFocus(pos, key, rowCount, colCount) {
6
+ const maxRow = Math.max(0, rowCount - 1);
7
+ const maxCol = Math.max(0, colCount - 1);
8
+ let row = pos.row;
9
+ let col = pos.col;
10
+ switch (key) {
11
+ case 'ArrowUp':
12
+ row -= 1;
13
+ break;
14
+ case 'ArrowDown':
15
+ row += 1;
16
+ break;
17
+ case 'ArrowLeft':
18
+ col -= 1;
19
+ break;
20
+ case 'ArrowRight':
21
+ col += 1;
22
+ break;
23
+ case 'Home':
24
+ col = 0;
25
+ break;
26
+ case 'End':
27
+ col = maxCol;
28
+ break;
29
+ default:
30
+ break;
31
+ }
32
+ return { row: clamp(row, 0, maxRow), col: clamp(col, 0, maxCol) };
33
+ }
@@ -0,0 +1,31 @@
1
+ export function moveItem(order, fromId, toIndex) {
2
+ const list = order ? order.slice() : [];
3
+ const from = list.indexOf(fromId);
4
+ if (from < 0) return list;
5
+ list.splice(from, 1);
6
+ const idx = Math.max(0, Math.min(toIndex, list.length));
7
+ list.splice(idx, 0, fromId);
8
+ return list;
9
+ }
10
+
11
+ export function applyRowOrder(rows, getRowId, rowOrderModel) {
12
+ if (!rowOrderModel || !rowOrderModel.length) return rows;
13
+ const rank = new Map(rowOrderModel.map((id, i) => [id, i]));
14
+ const tail = rowOrderModel.length;
15
+ return rows.slice().sort((a, b) => {
16
+ const ia = rank.has(getRowId(a)) ? rank.get(getRowId(a)) : tail;
17
+ const ib = rank.has(getRowId(b)) ? rank.get(getRowId(b)) : tail;
18
+ return ia - ib;
19
+ });
20
+ }
21
+
22
+ export function applyColumnOrder(columns, columnOrderModel) {
23
+ if (!columnOrderModel || !columnOrderModel.length) return columns;
24
+ const rank = new Map(columnOrderModel.map((field, i) => [field, i]));
25
+ const tail = columnOrderModel.length;
26
+ return columns.slice().sort((a, b) => {
27
+ const ia = rank.has(a.field) ? rank.get(a.field) : tail;
28
+ const ib = rank.has(b.field) ? rank.get(b.field) : tail;
29
+ return ia - ib;
30
+ });
31
+ }
@@ -0,0 +1,18 @@
1
+ export function pageCount(rowCount, pageSize) {
2
+ const size = Math.max(1, pageSize | 0);
3
+ const n = Math.max(0, rowCount | 0);
4
+ return Math.max(1, Math.ceil(n / size) || 1);
5
+ }
6
+
7
+ export function applyPagination(rows, page, pageSize) {
8
+ const size = Math.max(1, pageSize | 0);
9
+ const count = pageCount(rows.length, size);
10
+ const p = Math.max(0, Math.min(count - 1, page | 0));
11
+ const start = p * size;
12
+ return rows.slice(start, start + size);
13
+ }
14
+
15
+ export function clampPage(page, rowCount, pageSize) {
16
+ const count = pageCount(rowCount, pageSize);
17
+ return Math.max(0, Math.min(count - 1, page | 0));
18
+ }
@@ -0,0 +1,35 @@
1
+ export function toggleId(model, id) {
2
+ const list = model ? model.slice() : [];
3
+ const i = list.indexOf(id);
4
+ if (i >= 0) list.splice(i, 1);
5
+ else list.push(id);
6
+ return list;
7
+ }
8
+
9
+ /** Add or remove all visible ids from the selection model. */
10
+ export function setVisibleSelection(model, visibleIds, selected) {
11
+ const set = new Set(model || []);
12
+ for (const id of visibleIds) {
13
+ if (selected) set.add(id);
14
+ else set.delete(id);
15
+ }
16
+ return [...set];
17
+ }
18
+
19
+ export function selectableVisibleIds(rows, getRowId, isRowSelectable) {
20
+ return rows
21
+ .filter(r => !isRowSelectable || isRowSelectable(r))
22
+ .map(getRowId);
23
+ }
24
+
25
+ export function selectionState(visibleIds, model) {
26
+ if (!visibleIds.length) return 'none';
27
+ const set = new Set(model || []);
28
+ let n = 0;
29
+ for (const id of visibleIds) {
30
+ if (set.has(id)) n++;
31
+ }
32
+ if (n === 0) return 'none';
33
+ if (n === visibleIds.length) return 'all';
34
+ return 'some';
35
+ }
@@ -0,0 +1,91 @@
1
+ import { getCellValue } from './grid-columns.js';
2
+
3
+ function isEmpty(v) {
4
+ return v == null || v === '';
5
+ }
6
+
7
+ export function compareByType(a, b, type) {
8
+ const aEmpty = isEmpty(a);
9
+ const bEmpty = isEmpty(b);
10
+ if (aEmpty && bEmpty) return 0;
11
+ if (aEmpty) return 1;
12
+ if (bEmpty) return -1;
13
+ if (type === 'number') {
14
+ return Number(a) - Number(b);
15
+ }
16
+ if (type === 'boolean') {
17
+ return (a ? 1 : 0) - (b ? 1 : 0);
18
+ }
19
+ if (type === 'date') {
20
+ const ta = new Date(a).getTime();
21
+ const tb = new Date(b).getTime();
22
+ return ta - tb;
23
+ }
24
+ return String(a).localeCompare(String(b));
25
+ }
26
+
27
+ function compareRows(rowA, rowB, col, dir) {
28
+ if (col.sortComparator) {
29
+ return (
30
+ dir *
31
+ col.sortComparator(
32
+ getCellValue(rowA, col),
33
+ getCellValue(rowB, col),
34
+ rowA,
35
+ rowB
36
+ )
37
+ );
38
+ }
39
+ return (
40
+ dir *
41
+ compareByType(
42
+ getCellValue(rowA, col),
43
+ getCellValue(rowB, col),
44
+ col.type
45
+ )
46
+ );
47
+ }
48
+
49
+ export function applySort(rows, columns, sortModel) {
50
+ if (!sortModel || !sortModel.length) return rows;
51
+ const keys = [];
52
+ for (const item of sortModel) {
53
+ if (!item || !item.field) continue;
54
+ const col = columns.find(c => c.field === item.field);
55
+ if (!col) continue;
56
+ const dir = item.sort === 'desc' ? -1 : 1;
57
+ keys.push({ col, dir });
58
+ }
59
+ if (!keys.length) return rows;
60
+ const out = rows.slice();
61
+ out.sort((rowA, rowB) => {
62
+ for (const { col, dir } of keys) {
63
+ const r = compareRows(rowA, rowB, col, dir);
64
+ if (r !== 0) return r;
65
+ }
66
+ return 0;
67
+ });
68
+ return out;
69
+ }
70
+
71
+ /** Cycle none -> asc -> desc -> none for a field. */
72
+ export function cycleSortModel(model, field, opts) {
73
+ if (opts && opts.append) {
74
+ const list = model ? model.slice() : [];
75
+ const i = list.findIndex(item => item.field === field);
76
+ if (i < 0) return [...list, { field, sort: 'asc' }];
77
+ const cur = list[i];
78
+ if (cur.sort === 'asc') {
79
+ const next = list.slice();
80
+ next[i] = { field, sort: 'desc' };
81
+ return next;
82
+ }
83
+ const next = list.slice();
84
+ next.splice(i, 1);
85
+ return next;
86
+ }
87
+ const cur = model && model[0] && model[0].field === field ? model[0] : null;
88
+ if (!cur) return [{ field, sort: 'asc' }];
89
+ if (cur.sort === 'asc') return [{ field, sort: 'desc' }];
90
+ return [];
91
+ }
@@ -0,0 +1,17 @@
1
+ export function windowRange(
2
+ scrollTop,
3
+ viewportHeight,
4
+ rowCount,
5
+ rowHeight,
6
+ overscan = 3
7
+ ) {
8
+ if (rowCount <= 0 || rowHeight <= 0) {
9
+ return { start: 0, end: 0, offsetTop: 0, totalHeight: 0 };
10
+ }
11
+ const totalHeight = rowCount * rowHeight;
12
+ const visibleStart = Math.floor(Math.max(0, scrollTop) / rowHeight);
13
+ const visibleCount = Math.ceil(Math.max(0, viewportHeight) / rowHeight);
14
+ const start = Math.max(0, visibleStart - overscan);
15
+ const end = Math.min(rowCount, visibleStart + visibleCount + overscan);
16
+ return { start, end, offsetTop: start * rowHeight, totalHeight };
17
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "strike-fw-datagrid",
3
+ "version": "0.1.0",
4
+ "description": "DataGrid for Strike; peers on strike-fw and strike-fw-ui",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Jahred Love",
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "sideEffects": [
12
+ "./data-grid.js"
13
+ ],
14
+ "main": "./index.js",
15
+ "types": "./types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./types/index.d.ts",
19
+ "import": "./index.js",
20
+ "default": "./index.js"
21
+ },
22
+ "./package.json": "./package.json",
23
+ "./*": {
24
+ "types": "./types/*.d.ts",
25
+ "import": "./*",
26
+ "default": "./*"
27
+ }
28
+ },
29
+ "files": [
30
+ "index.js",
31
+ "data-grid.js",
32
+ "lib",
33
+ "types",
34
+ "LICENSE",
35
+ "README.md",
36
+ "CHANGELOG.md"
37
+ ],
38
+ "scripts": {
39
+ "test": "node --test test/*.js"
40
+ },
41
+ "peerDependencies": {
42
+ "strike-fw": ">=0.2.1",
43
+ "strike-fw-ui": ">=0.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "linkedom": "^0.18.0",
47
+ "strike-fw": "^0.2.1",
48
+ "strike-fw-ui": "^0.2.0"
49
+ }
50
+ }
@@ -0,0 +1,80 @@
1
+ export declare function DataGrid(props: {
2
+ columns: Record<string, unknown>[];
3
+ rows?: Record<string, unknown>[];
4
+ getRowId: (row: Record<string, unknown>) => string | number;
5
+ processRowUpdate?: (
6
+ newRow: Record<string, unknown>,
7
+ oldRow: Record<string, unknown>
8
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
9
+ onProcessRowUpdateError?: (err: unknown) => void;
10
+ caption?: unknown;
11
+ class?: string;
12
+ density?: 'sm' | 'md' | string;
13
+ striped?: boolean | 'rows' | 'columns' | 'both';
14
+ stripedRowScope?: 'page' | 'dataset';
15
+ headerShade?: boolean | 'muted' | 'none';
16
+ stickyHeader?: boolean;
17
+ getRowClassName?: (row: Record<string, unknown>) => string | null | undefined;
18
+ checkboxSelection?: boolean;
19
+ isRowSelectable?: (row: Record<string, unknown>) => boolean;
20
+ disableColumnSorting?: boolean;
21
+ disableQuickFilter?: boolean;
22
+ hideFooter?: boolean;
23
+ loading?: boolean;
24
+ loadingOverlay?: unknown;
25
+ empty?: unknown;
26
+ toolbar?: unknown;
27
+ quickFilterPlaceholder?: string;
28
+ pageSizeOptions?: number[];
29
+ sortingMode?: 'client' | 'server';
30
+ filterMode?: 'client' | 'server';
31
+ paginationMode?: 'client' | 'server';
32
+ rowCount?: number;
33
+ sortModel?: { field: string; sort: 'asc' | 'desc' }[];
34
+ defaultSortModel?: { field: string; sort: 'asc' | 'desc' }[];
35
+ onSortModelChange?: (model: { field: string; sort: 'asc' | 'desc' }[]) => void;
36
+ quickFilterValue?: string;
37
+ defaultQuickFilterValue?: string;
38
+ onQuickFilterValueChange?: (value: string) => void;
39
+ filterModel?: {
40
+ items: Array<{ field: string; operator: string; value?: unknown }>;
41
+ };
42
+ defaultFilterModel?: {
43
+ items: Array<{ field: string; operator: string; value?: unknown }>;
44
+ };
45
+ onFilterModelChange?: (model: {
46
+ items: Array<{ field: string; operator: string; value?: unknown }>;
47
+ }) => void;
48
+ paginationModel?: { page: number; pageSize: number };
49
+ defaultPaginationModel?: { page: number; pageSize: number };
50
+ onPaginationModelChange?: (model: { page: number; pageSize: number }) => void;
51
+ selectionModel?: Array<string | number>;
52
+ defaultSelectionModel?: Array<string | number>;
53
+ onSelectionModelChange?: (model: Array<string | number>) => void;
54
+ columnVisibilityModel?: Record<string, boolean>;
55
+ defaultColumnVisibilityModel?: Record<string, boolean>;
56
+ onColumnVisibilityModelChange?: (model: Record<string, boolean>) => void;
57
+ editCell?: { id: string | number; field: string | null } | null;
58
+ defaultEditCell?: { id: string | number; field: string | null } | null;
59
+ onEditCellChange?: (
60
+ cell: { id: string | number; field: string | null } | null
61
+ ) => void;
62
+ editMode?: 'cell' | 'row';
63
+ editOnClick?: boolean;
64
+ enableGridKeyboard?: boolean;
65
+ rowOrderModel?: Array<string | number>;
66
+ defaultRowOrderModel?: Array<string | number>;
67
+ onRowOrderChange?: (model: Array<string | number>) => void;
68
+ columnOrderModel?: string[];
69
+ defaultColumnOrderModel?: string[];
70
+ onColumnOrderChange?: (model: string[]) => void;
71
+ columnWidthModel?: Record<string, number>;
72
+ defaultColumnWidthModel?: Record<string, number>;
73
+ onColumnWidthChange?: (model: Record<string, number>) => void;
74
+ disableColumnResize?: boolean;
75
+ rowReorderMode?: 'client' | 'server';
76
+ virtualize?: boolean;
77
+ getRowHeight?: number;
78
+ onRowClick?: (row: Record<string, unknown>, ev: unknown) => void;
79
+ [key: string]: unknown;
80
+ }): unknown;
@@ -0,0 +1,7 @@
1
+ export { DataGrid } from './data-grid.js';
2
+ export { cls } from 'strike-fw/ui';
3
+
4
+ export declare function rowsToCsv(
5
+ rows: Record<string, unknown>[],
6
+ columns: Array<{ field: string; headerName?: string }>
7
+ ): string;