@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.
@@ -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 }