@rozie-ui/data-table-solid 0.2.6 → 0.3.2
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/README.md +3 -2
- package/dist/index.cjs +696 -255
- package/dist/index.d.cts +7 -3
- package/dist/index.d.mts +7 -3
- package/dist/index.mjs +696 -255
- package/package.json +2 -2
- package/src/DataTable.tsx +1320 -418
- package/src/helpers/columnDefUtils.ts +111 -0
- package/src/helpers/indexMath.ts +25 -0
- package/src/helpers/rowValueUtils.ts +62 -0
- package/src/helpers/tsvGrid.ts +115 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// columnDefUtils.ts — pure column-def helpers extracted from columnBuilders.rzts
|
|
2
|
+
// (D-22, Phase 87 plan 87-01). Sigil-free, self-contained: no $props/$data/$emit/
|
|
3
|
+
// $computed/$onMount/$refs/$el/$expose/$watch, and no cross-partial host-symbol
|
|
4
|
+
// calls. Moved verbatim (bodies + explanatory comments intact) — behavior-neutral
|
|
5
|
+
// extraction, proven by dist-parity zero drift + the data-table VR gate.
|
|
6
|
+
|
|
7
|
+
// Prototype-safe id-keyed column resolution (T-48-PP): the `:columns` config array is
|
|
8
|
+
// applied FIRST (lower precedence), then the <Column> registry OVERRIDES by id (LWW).
|
|
9
|
+
// byId is a null-prototype object so a consumer column id of "__proto__"/"constructor"
|
|
10
|
+
// cannot pollute Object.prototype. Returns the table-core ColumnDef[]. (No per-column
|
|
11
|
+
// render callbacks — cells render via the single #cell/#header scoped slot on this
|
|
12
|
+
// component, dispatched by columnId; <Column> carries metadata only.)
|
|
13
|
+
const isSafeKey = (k: any) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype'
|
|
14
|
+
|
|
15
|
+
// wrapAggregationFn (phase 50 req-5, D-05, threat T-50-04): resolve a per-column
|
|
16
|
+
// aggregationFn straight onto the ColumnDef (no component-side switch — RESEARCH
|
|
17
|
+
// anti-pattern). A built-in NAME string ('sum'/'min'/'max'/'extent'/'mean'/'median'/
|
|
18
|
+
// 'unique'/'uniqueCount'/'count') passes through verbatim — table-core resolves it from its
|
|
19
|
+
// built-in `aggregationFns` map. A CUSTOM function `(columnId, leafRows, childRows) => any`
|
|
20
|
+
// is DEFENSIVELY WRAPPED (the runValidator precedent): a consumer fn runs per group, so a
|
|
21
|
+
// throw is coerced to `undefined` and can never crash getGroupedRowModel (DoS guard).
|
|
22
|
+
// Anything else → undefined (no aggregation; the cell renders as a placeholder).
|
|
23
|
+
const wrapAggregationFn = (fn: any) => {
|
|
24
|
+
if (typeof fn === 'string') return fn
|
|
25
|
+
if (typeof fn !== 'function') return undefined
|
|
26
|
+
return (columnId: any, leafRows: any, childRows: any) => {
|
|
27
|
+
try {
|
|
28
|
+
return fn(columnId, leafRows, childRows)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── B1 (quick 260906-afh) — nested group leaf def index ─────────────────────────────
|
|
36
|
+
// LOOKUP-ONLY. This map is never fed to table-core (it does not replace/reshape the
|
|
37
|
+
// columnDefs() array table-core consumes for createTable/setOptions — that stays
|
|
38
|
+
// byte-identical); it exists purely so `defFor(colId)` (columnChrome.rzts) can resolve
|
|
39
|
+
// a NESTED leaf's def (a `columns:` group child) in O(1), the same as a top-level leaf.
|
|
40
|
+
//
|
|
41
|
+
// collectNestedDefs: depth-first walk of a `columns` array (each entry may itself carry
|
|
42
|
+
// a nested `columns` array — 3+ levels deep is supported). Writes `out[String(d.id)] = d`
|
|
43
|
+
// ONLY when the id is ABSENT from `out` — first-write-wins so a caller can pre-seed `out`
|
|
44
|
+
// with the higher-precedence top-level defs and this pass only ever fills GAPS.
|
|
45
|
+
const collectNestedDefs = (list: any, out: any) => {
|
|
46
|
+
if (!Array.isArray(list)) return
|
|
47
|
+
for (const d of list) {
|
|
48
|
+
if (!d || d.id == null) continue
|
|
49
|
+
const id = String(d.id)
|
|
50
|
+
if (!(id in out)) out[id] = d
|
|
51
|
+
if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// indexDefsById: builds a null-prototype `{ id: def }` map from a `columnDefs()`-shaped
|
|
56
|
+
// array in TWO passes. Pass 1 registers every TOP-LEVEL def unconditionally — so any id
|
|
57
|
+
// that already resolves at HEAD (a top-level leaf OR a top-level group's OWN id) keeps
|
|
58
|
+
// resolving to the EXACT SAME def, the byte-identical-lookup guarantee for every
|
|
59
|
+
// pre-existing caller. Pass 2 walks each top-level entry's `columns` (if any) via
|
|
60
|
+
// collectNestedDefs, so a nested leaf only fills a GAP — it can never shadow a
|
|
61
|
+
// same-named top-level id (top-level wins regardless of array order). Object.create(null)
|
|
62
|
+
// (T-AFH-01): a consumer column id of "__proto__"/"constructor" cannot pollute
|
|
63
|
+
// Object.prototype through this map — mirrors the existing `byId` T-48-PP guard in
|
|
64
|
+
// columnBuilders.rzts.
|
|
65
|
+
const indexDefsById = (defs: any): any => {
|
|
66
|
+
const out = Object.create(null)
|
|
67
|
+
if (!Array.isArray(defs)) return out
|
|
68
|
+
for (const d of defs) {
|
|
69
|
+
if (!d || d.id == null) continue
|
|
70
|
+
out[String(d.id)] = d
|
|
71
|
+
}
|
|
72
|
+
for (const d of defs) {
|
|
73
|
+
if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
|
|
74
|
+
}
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── C5+C6 (quick 260906-cvo) — groupable-leaf collection for the #groupBar ──────────────
|
|
79
|
+
// C5: buildConfigDef's GROUP branch (columnBuilders.rzts) returns `{ id, header, columns }`
|
|
80
|
+
// with NO `groupable` key and no accessor, so the OLD `groupableColumns()` loop's
|
|
81
|
+
// `d.groupable === false` skip never fired for a group column — it was wrongly OFFERED to
|
|
82
|
+
// the bar even though a group column can never produce a grouping value.
|
|
83
|
+
// C6: that same loop walked TOP-LEVEL `columnDefs()` entries only, so a genuinely groupable
|
|
84
|
+
// NESTED leaf (a `columns:` group child) was never offered at all.
|
|
85
|
+
//
|
|
86
|
+
// collectGroupableLeafDefs: a DEDICATED depth-first walk, NOT a reuse of `indexDefsById`.
|
|
87
|
+
// `indexDefsById` is a LOOKUP index — it deliberately registers group columns too (a group
|
|
88
|
+
// id must keep resolving for `defFor`), and its insertion order is all-top-level-first-then-
|
|
89
|
+
// nested, which would both offer group columns AND hand the group bar an order that does not
|
|
90
|
+
// match the rendered header order. This walk instead: recurses into a `columns:` array
|
|
91
|
+
// WITHOUT ever pushing the group entry itself (closes C5 — a group column has no accessor
|
|
92
|
+
// and cannot be a grouping target, so an explicit `groupable: true` on a group entry is
|
|
93
|
+
// meaningless and is likewise never pushed); pushes every other (leaf) entry unless it opts
|
|
94
|
+
// out via `groupable === false` (closes C6 — nested leaves are now reachable); walks in
|
|
95
|
+
// DECLARATION order, depth-first, matching the rendered header order.
|
|
96
|
+
const collectGroupableLeafDefs = (defs: any): any[] => {
|
|
97
|
+
const out: any[] = []
|
|
98
|
+
if (!Array.isArray(defs)) return out
|
|
99
|
+
for (const d of defs) {
|
|
100
|
+
if (!d) continue
|
|
101
|
+
if (Array.isArray(d.columns)) {
|
|
102
|
+
out.push(...collectGroupableLeafDefs(d.columns))
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (d.groupable === false) continue
|
|
106
|
+
out.push(d)
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { isSafeKey, wrapAggregationFn, collectNestedDefs, indexDefsById, collectGroupableLeafDefs }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// indexMath.ts — pure index-math + DOM-query helpers extracted from gridFocusNav.rzts /
|
|
2
|
+
// writeFunnels.rzts (D-22, Phase 87 plan 87-01). Sigil-free, self-contained: no
|
|
3
|
+
// $props/$data/$emit/$computed/$onMount/$refs/$el/$expose/$watch, and no cross-partial
|
|
4
|
+
// host-symbol calls. Moved verbatim (bodies + explanatory comments intact) —
|
|
5
|
+
// behavior-neutral extraction, proven by dist-parity zero drift + the data-table VR gate.
|
|
6
|
+
// `focusables(cellEl)` reads the DOM but receives its element as a parameter and touches
|
|
7
|
+
// no sigil and no component ref, so it is framework-agnostic and belongs here alongside
|
|
8
|
+
// `clamp` — its querySelectorAll selector string and its `!n.disabled` filter are kept
|
|
9
|
+
// byte-identical to the pre-extraction body.
|
|
10
|
+
|
|
11
|
+
const clamp = (v: any, lo: any, hi: any) => (v < lo ? lo : (v > hi ? hi : v))
|
|
12
|
+
|
|
13
|
+
// The focusable descendants of a cell (non-disabled), in DOM order. Pure DOM — uniform ×6.
|
|
14
|
+
const focusables = (cellEl: any) => {
|
|
15
|
+
if (!cellEl || !cellEl.querySelectorAll) return []
|
|
16
|
+
const list = Array.prototype.slice.call(
|
|
17
|
+
cellEl.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')
|
|
18
|
+
)
|
|
19
|
+
return list.filter((n) => !n.disabled)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const applyUpdater = (updater: any, current: any) =>
|
|
23
|
+
(typeof updater === 'function' ? updater(current) : updater)
|
|
24
|
+
|
|
25
|
+
export { clamp, focusables, applyUpdater }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// rowValueUtils.ts — pure row-value helpers extracted from editCellLifecycle.rzts /
|
|
2
|
+
// editRowLifecycle.rzts (D-22, Phase 87 plan 87-01). Sigil-free, self-contained: no
|
|
3
|
+
// $props/$data/$emit/$computed/$onMount/$refs/$el/$expose/$watch, and no cross-partial
|
|
4
|
+
// host-symbol calls. Moved verbatim (bodies + explanatory comments intact) —
|
|
5
|
+
// behavior-neutral extraction, proven by dist-parity zero drift + the data-table VR gate.
|
|
6
|
+
|
|
7
|
+
// replaceRowValue: build a FRESH array with ONE row object replaced (the column's field
|
|
8
|
+
// set to the new value); the rest share by reference (the family immutable whole-array
|
|
9
|
+
// replace — in-place mutation is silently dropped on React/Solid/Angular/Lit). rowIndex
|
|
10
|
+
// is over currentData() (== the visible model order for the non-virtual, unsorted/
|
|
11
|
+
// unfiltered single-cell case; the row id is carried for the commit payload).
|
|
12
|
+
const replaceRowValue = (rows: any, rowIndex: any, field: any, value: any) => {
|
|
13
|
+
const src = rows || []
|
|
14
|
+
const out = []
|
|
15
|
+
for (let i = 0; i < src.length; i++) {
|
|
16
|
+
if (i === rowIndex) {
|
|
17
|
+
// WR-03: own-property spread, NOT `for (const k in orig)` which walks the prototype chain
|
|
18
|
+
// and would copy inherited enumerable props of typed/class-instance row objects.
|
|
19
|
+
out.push({ ...(src[i] || {}), [field]: value })
|
|
20
|
+
} else {
|
|
21
|
+
out.push(src[i])
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// replaceRowValues: like replaceRowValue but applies a MAP of field→value to ONE row object
|
|
28
|
+
// in a single fresh-array replace (req-6 — the whole-row commit is ONE write, not per cell).
|
|
29
|
+
const replaceRowValues = (rows: any, rowIndex: any, fieldValues: any) => {
|
|
30
|
+
const src = rows || []
|
|
31
|
+
const fv = fieldValues || {}
|
|
32
|
+
const out = []
|
|
33
|
+
for (let i = 0; i < src.length; i++) {
|
|
34
|
+
if (i === rowIndex) {
|
|
35
|
+
// WR-03: own-property spread (orig then the field→value map), NOT a `for..in`
|
|
36
|
+
// prototype-walking copy. Spread copies own enumerable props only.
|
|
37
|
+
out.push({ ...(src[i] || {}), ...fv })
|
|
38
|
+
} else {
|
|
39
|
+
out.push(src[i])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// B23: the index of a committed row WITHIN a given (fresh) visible-model array, resolved by
|
|
46
|
+
// row IDENTITY. table-core's default getRowId is source-index-based, so a row's id is stable
|
|
47
|
+
// across a re-sort (only its VISIBLE position moves); a committed edit replaces the row object
|
|
48
|
+
// via a fresh spread (the `original` reference changes), so match by `id` FIRST, `original`
|
|
49
|
+
// only as a fallback. Returns -1 when the row filtered out of the view. PURE (the caller passes
|
|
50
|
+
// the FRESH row list — refreshRowModel's just-pulled `nextRows`, never the React-stale state).
|
|
51
|
+
const indexOfRowIn = (rows: any, rowOriginal: any, rowId: any) => {
|
|
52
|
+
const list = rows || []
|
|
53
|
+
for (let i = 0; i < list.length; i++) {
|
|
54
|
+
const r = list[i]
|
|
55
|
+
if (!r) continue
|
|
56
|
+
if (rowId != null && r.id === rowId) return i
|
|
57
|
+
if (rowOriginal != null && r.original === rowOriginal) return i
|
|
58
|
+
}
|
|
59
|
+
return -1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { replaceRowValue, replaceRowValues, indexOfRowIn }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// tsvGrid.ts — pure TSV/clipboard-grid helpers extracted from clipboardFill.rzts
|
|
2
|
+
// (D-22, Phase 87 plan 87-01). Sigil-free, self-contained: no $props/$data/$emit/
|
|
3
|
+
// $computed/$onMount/$refs/$el/$expose/$watch, and no cross-partial host-symbol
|
|
4
|
+
// calls. Moved verbatim (bodies + explanatory comments intact) — behavior-neutral
|
|
5
|
+
// extraction, proven by dist-parity zero drift + the data-table VR gate.
|
|
6
|
+
|
|
7
|
+
// B10: escape a TSV field per the spreadsheet convention — a field containing a tab, a CR/LF,
|
|
8
|
+
// or a double-quote is wrapped in double-quotes with internal quotes DOUBLED; an ordinary
|
|
9
|
+
// field is emitted verbatim. parseTsv() unescapes symmetrically, so a cell carrying a tab /
|
|
10
|
+
// newline / quote round-trips without smearing into adjacent cells (T-63-03-02).
|
|
11
|
+
const escapeTsvField = (s: any) => {
|
|
12
|
+
if (s.indexOf('\t') >= 0 || s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('"') >= 0) {
|
|
13
|
+
return '"' + s.replace(/"/g, '""') + '"'
|
|
14
|
+
}
|
|
15
|
+
return s
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// parseTsv(text): a TSV string → string[][] (rows of cells). Tolerates \r\n; a trailing
|
|
19
|
+
// newline does not add a phantom empty row. Pure — produces plain string DATA only (T-51-01:
|
|
20
|
+
// the cells are NEVER eval'd / interpolated into a selector / rendered as markup).
|
|
21
|
+
const parseTsv = (text: any) => {
|
|
22
|
+
const str = text != null ? String(text) : ''
|
|
23
|
+
// CR-03: length guard BEFORE the parse — an empty string is a no-op, and a pathologically
|
|
24
|
+
// large clipboard payload (>2M chars) is rejected outright (DoS-shaped input) before the
|
|
25
|
+
// single-pass scan allocates a cell-per-character grid.
|
|
26
|
+
if (str === '' || str.length > 2000000) return []
|
|
27
|
+
// B10: a quote-aware single-pass state machine (replaces the naive split, which corrupted a
|
|
28
|
+
// cell containing a tab/newline). A field that OPENS with a double-quote is "quoted": tabs,
|
|
29
|
+
// newlines, and doubled quotes ("") inside it are literal content until the closing quote;
|
|
30
|
+
// an unquoted field ends at the next tab/newline. CR/LF and CRLF all delimit a row.
|
|
31
|
+
const rows = []
|
|
32
|
+
let row = []
|
|
33
|
+
let field = ''
|
|
34
|
+
let inQuotes = false
|
|
35
|
+
let i = 0
|
|
36
|
+
const n = str.length
|
|
37
|
+
while (i < n) {
|
|
38
|
+
const ch = str[i]
|
|
39
|
+
if (inQuotes) {
|
|
40
|
+
if (ch === '"') {
|
|
41
|
+
if (i + 1 < n && str[i + 1] === '"') { field = field + '"'; i = i + 2; continue }
|
|
42
|
+
inQuotes = false; i = i + 1; continue
|
|
43
|
+
}
|
|
44
|
+
field = field + ch; i = i + 1; continue
|
|
45
|
+
}
|
|
46
|
+
if (ch === '"' && field === '') { inQuotes = true; i = i + 1; continue }
|
|
47
|
+
if (ch === '\t') { row.push(field); field = ''; i = i + 1; continue }
|
|
48
|
+
if (ch === '\r') {
|
|
49
|
+
if (i + 1 < n && str[i + 1] === '\n') i = i + 1
|
|
50
|
+
row.push(field); field = ''; rows.push(row); row = []; i = i + 1; continue
|
|
51
|
+
}
|
|
52
|
+
if (ch === '\n') { row.push(field); field = ''; rows.push(row); row = []; i = i + 1; continue }
|
|
53
|
+
field = field + ch; i = i + 1
|
|
54
|
+
}
|
|
55
|
+
// Flush the trailing field + row.
|
|
56
|
+
row.push(field)
|
|
57
|
+
rows.push(row)
|
|
58
|
+
// Drop a single trailing empty row (a TSV that ends with a newline → a phantom [''] row).
|
|
59
|
+
if (rows.length > 1) {
|
|
60
|
+
const last = rows[rows.length - 1]
|
|
61
|
+
if (last.length === 1 && last[0] === '') rows.pop()
|
|
62
|
+
}
|
|
63
|
+
return rows
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// C3: tile a parsed clipboard `grid` (string[][]) to fill a destination `box` — the spreadsheet
|
|
67
|
+
// paste-into-range semantics. The target rectangle is the MAX of the box dims and the source
|
|
68
|
+
// dims per axis, so a SMALLER clipboard TILES across a LARGER selection (a single 1×1 cell fills
|
|
69
|
+
// the whole range; a 2×2 block repeats — tiled[dr][dc] = src[dr % srcRows][dc % srcCols]), while a
|
|
70
|
+
// clipboard LARGER than the selection pastes its full block from the top-left (preserving the
|
|
71
|
+
// no-range "clipboard-sized block at the active cell" behavior — a 1×1 destBox + a 1×N clipboard
|
|
72
|
+
// yields the full 1×N block, byte-for-byte the prior path). Pure — returns a fresh grid; applies
|
|
73
|
+
// nothing. A ragged/short source row defaults the missing cell to '' (coerced per column on write).
|
|
74
|
+
const tileGridToBox = (grid: any, box: any) => {
|
|
75
|
+
const srcRows = grid.length
|
|
76
|
+
// srcCols is the MAX row width across ALL rows (not grid[0].length): a RAGGED clipboard
|
|
77
|
+
// (a later row WIDER than the first, e.g. TSV "a\tb\nc\td\te") would otherwise never read
|
|
78
|
+
// the extra column and silently drop those cells. A row SHORTER than srcCols tiles its
|
|
79
|
+
// missing cells as '' (the `v != null ? v : ''` coercion below), never undefined.
|
|
80
|
+
let srcCols = 0
|
|
81
|
+
for (let i = 0; i < srcRows; i++) {
|
|
82
|
+
const w = grid[i] && grid[i].length ? grid[i].length : 0
|
|
83
|
+
if (w > srcCols) srcCols = w
|
|
84
|
+
}
|
|
85
|
+
if (srcRows <= 0 || srcCols <= 0) return grid
|
|
86
|
+
const boxRows = (box.r1 - box.r0) + 1
|
|
87
|
+
const boxCols = (box.c1 - box.c0) + 1
|
|
88
|
+
const rows = boxRows > srcRows ? boxRows : srcRows
|
|
89
|
+
const cols = boxCols > srcCols ? boxCols : srcCols
|
|
90
|
+
const out = []
|
|
91
|
+
for (let r = 0; r < rows; r++) {
|
|
92
|
+
const srcLine = grid[r % srcRows] || []
|
|
93
|
+
const line = []
|
|
94
|
+
for (let c = 0; c < cols; c++) {
|
|
95
|
+
const v = srcLine[c % srcCols]
|
|
96
|
+
line.push(v != null ? v : '')
|
|
97
|
+
}
|
|
98
|
+
out.push(line)
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// tileIndex(i, lo, hi): map an index into the inclusive [lo,hi] source span by TILING (repeat
|
|
104
|
+
// the source block), handling indices below lo (negative offset) correctly. A 1-wide source
|
|
105
|
+
// (lo===hi) always returns lo. Used by fillRange to resolve, per target cell, WHICH source
|
|
106
|
+
// cell it copies — so each column copies its OWN source value down its OWN column.
|
|
107
|
+
const tileIndex = (i: any, lo: any, hi: any) => {
|
|
108
|
+
const span = (hi - lo) + 1
|
|
109
|
+
if (span <= 1) return lo
|
|
110
|
+
let k = (i - lo) % span
|
|
111
|
+
if (k < 0) k = k + span
|
|
112
|
+
return lo + k
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { escapeTsvField, parseTsv, tileGridToBox, tileIndex }
|