@svelte-lean/table 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/dist/export.js ADDED
@@ -0,0 +1,68 @@
1
+ import { valueOf } from './core.js';
2
+ export function toCsv(rows, columns, options = {}) {
3
+ const delimiter = options.delimiter ?? ',';
4
+ const escape = (value) => {
5
+ let text = value instanceof Date ? value.toISOString() : String(value ?? '');
6
+ if (options.protectFormulas !== false && /^[\s]*[=+\-@\t\r]/.test(text))
7
+ text = `'${text}`;
8
+ return /["\r\n]/.test(text) || text.includes(delimiter)
9
+ ? `"${text.replaceAll('"', '""')}"`
10
+ : text;
11
+ };
12
+ const output = options.headers === false ? [] : [columns.map((c) => escape(c.header)).join(delimiter)];
13
+ for (const row of rows)
14
+ output.push(columns
15
+ .map((c) => escape(c.format ? c.format(valueOf(row, c), row) : valueOf(row, c)))
16
+ .join(delimiter));
17
+ return (options.bom === false ? '' : '\uFEFF') + output.join('\r\n');
18
+ }
19
+ export function downloadText(content, filename, type = 'text/csv;charset=utf-8') {
20
+ const url = URL.createObjectURL(new Blob([content], { type }));
21
+ const link = document.createElement('a');
22
+ link.href = url;
23
+ link.download = filename;
24
+ link.click();
25
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
26
+ }
27
+ /** RFC-style quoted cells, including embedded line breaks and escaped quotes. */
28
+ export function parseDelimited(text, delimiter = ',') {
29
+ const rows = [];
30
+ let row = [];
31
+ let cell = '';
32
+ let quoted = false;
33
+ const source = text.replace(/^\uFEFF/, '');
34
+ for (let i = 0; i < source.length; i++) {
35
+ const char = source[i];
36
+ if (char === '"') {
37
+ if (quoted && source[i + 1] === '"') {
38
+ cell += '"';
39
+ i++;
40
+ }
41
+ else if (!quoted && cell.length > 0)
42
+ cell += char;
43
+ else
44
+ quoted = !quoted;
45
+ }
46
+ else if (char === delimiter && !quoted) {
47
+ row.push(cell);
48
+ cell = '';
49
+ }
50
+ else if ((char === '\n' || char === '\r') && !quoted) {
51
+ if (char === '\r' && source[i + 1] === '\n')
52
+ i++;
53
+ row.push(cell);
54
+ rows.push(row);
55
+ row = [];
56
+ cell = '';
57
+ }
58
+ else
59
+ cell += char;
60
+ }
61
+ if (quoted)
62
+ throw new Error('Unclosed quoted field.');
63
+ if (cell.length || row.length) {
64
+ row.push(cell);
65
+ rows.push(row);
66
+ }
67
+ return rows;
68
+ }
@@ -0,0 +1,5 @@
1
+ import type { Aggregate, ModelContext, RowModel } from './types.js';
2
+ export declare function aggregate(values: unknown[], operation?: Aggregate): unknown;
3
+ /** Optional model: register by passing getRowModel={groupBy(['team'])}. */
4
+ export declare function groupBy<T>(ids: readonly string[]): (context: ModelContext<T>) => RowModel<T>;
5
+ export declare function summarize<T>(rows: readonly T[], columns: readonly import('./types.js').Column<T>[]): Record<string, unknown>;
@@ -0,0 +1,71 @@
1
+ import { leafColumns, paginateNodes, prepareRows, valueOf } from './core.js';
2
+ export function aggregate(values, operation = 'count') {
3
+ if (typeof operation === 'function')
4
+ return operation(values);
5
+ if (operation === 'count')
6
+ return values.length;
7
+ const numbers = values.filter((v) => typeof v === 'number' && Number.isFinite(v));
8
+ if (!numbers.length)
9
+ return operation === 'sum' ? 0 : null;
10
+ if (operation === 'min')
11
+ return numbers.reduce((a, b) => Math.min(a, b));
12
+ if (operation === 'max')
13
+ return numbers.reduce((a, b) => Math.max(a, b));
14
+ const sum = numbers.reduce((a, b) => a + b, 0);
15
+ return operation === 'mean' ? sum / numbers.length : sum;
16
+ }
17
+ /** Optional model: register by passing getRowModel={groupBy(['team'])}. */
18
+ export function groupBy(ids) {
19
+ return (context) => {
20
+ if (context.getSubRows)
21
+ throw new Error('Grouping and nested tree input are separate row models.');
22
+ const columns = leafColumns(context.columns);
23
+ const roots = prepareRows(context);
24
+ const occupied = new Set(roots.map((row) => row.id));
25
+ const build = (nodes, depth, path) => {
26
+ const column = columns.find((c) => c.id === ids[depth]);
27
+ if (!column)
28
+ return nodes.map((node) => ({ ...node, depth }));
29
+ const buckets = new Map();
30
+ for (const node of nodes) {
31
+ if (node.data === undefined)
32
+ continue;
33
+ const key = valueOf(node.data, column);
34
+ const bucket = buckets.get(key) ?? [];
35
+ bucket.push(node);
36
+ buckets.set(key, bucket);
37
+ }
38
+ return [...buckets].map(([key, members]) => {
39
+ const nextPath = [...path, `${column.id}:${typeof key}:${JSON.stringify(key)}`];
40
+ let id = `__group:${JSON.stringify(nextPath)}`;
41
+ while (occupied.has(id))
42
+ id += ':';
43
+ occupied.add(id);
44
+ const aggregates = Object.fromEntries(columns
45
+ .filter((c) => c.aggregate)
46
+ .map((c) => [
47
+ c.id,
48
+ aggregate(members.map((n) => valueOf(n.data, c)), c.aggregate)
49
+ ]));
50
+ return {
51
+ id,
52
+ kind: 'group',
53
+ label: `${column.header}: ${String(key ?? '—')}`,
54
+ depth,
55
+ count: members.length,
56
+ aggregates,
57
+ children: build(members, depth + 1, nextPath)
58
+ };
59
+ });
60
+ };
61
+ return paginateNodes(build(roots, 0, []), context);
62
+ };
63
+ }
64
+ export function summarize(rows, columns) {
65
+ return Object.fromEntries(leafColumns(columns)
66
+ .filter((c) => c.aggregate)
67
+ .map((c) => [
68
+ c.id,
69
+ aggregate(rows.map((row) => valueOf(row, c)), c.aggregate)
70
+ ]));
71
+ }
@@ -0,0 +1,7 @@
1
+ export { default as DataTable } from './table/data-table.svelte';
2
+ export { en, tr } from './messages.js';
3
+ export { defaultState, createRowModel, valueOf, visibleColumns, toggleSort, toggleSelection } from './core.js';
4
+ export { createTable } from './table.svelte.js';
5
+ export type { MaybeGetter, SelectionMode, Table, TableOptions } from './table.svelte.js';
6
+ export type { HeaderCell } from './core.js';
7
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as DataTable } from './table/data-table.svelte';
2
+ export { en, tr } from './messages.js';
3
+ export { defaultState, createRowModel, valueOf, visibleColumns, toggleSort, toggleSelection } from './core.js';
4
+ export { createTable } from './table.svelte.js';
@@ -0,0 +1,3 @@
1
+ import type { TableMessages } from './types.js';
2
+ export declare const en: TableMessages;
3
+ export declare const tr: TableMessages;
@@ -0,0 +1,72 @@
1
+ export const en = {
2
+ search: 'Search rows',
3
+ columns: 'Columns',
4
+ selectAll: 'Select all eligible rows',
5
+ selectRow: 'Select row',
6
+ expand: 'Expand row',
7
+ collapse: 'Collapse row',
8
+ previous: 'Previous page',
9
+ next: 'Next page',
10
+ first: 'First page',
11
+ last: 'Last page',
12
+ pageSize: 'Rows per page',
13
+ loading: 'Loading rows…',
14
+ empty: 'No matching rows',
15
+ retry: 'Retry',
16
+ save: 'Save',
17
+ cancel: 'Cancel',
18
+ edit: 'Edit',
19
+ resize: 'Resize',
20
+ moveLeft: 'Move earlier',
21
+ moveRight: 'Move later',
22
+ moveUp: 'Move up',
23
+ moveDown: 'Move down',
24
+ pin: 'Pin',
25
+ unpin: 'Unpinned',
26
+ pinLeft: 'Pin start',
27
+ pinRight: 'Pin end',
28
+ allRows: 'All',
29
+ sortOrder: 'Sort order',
30
+ selectionHeader: 'Selection',
31
+ expandHeader: 'Details',
32
+ filter: (column) => `Filter ${column}`,
33
+ page: (page, pages, rows) => rows === undefined ? `Page ${page} of ${pages}` : `Page ${page} of ${pages} · ${rows} rows`,
34
+ pageNumber: (page) => `Page ${page}`,
35
+ selected: (count) => `${count} selected`
36
+ };
37
+ export const tr = {
38
+ search: 'Satırlarda ara',
39
+ columns: 'Sütunlar',
40
+ selectAll: 'Uygun satırların tümünü seç',
41
+ selectRow: 'Satırı seç',
42
+ expand: 'Satırı aç',
43
+ collapse: 'Satırı kapat',
44
+ previous: 'Önceki sayfa',
45
+ next: 'Sonraki sayfa',
46
+ first: 'İlk sayfa',
47
+ last: 'Son sayfa',
48
+ pageSize: 'Sayfadaki satır',
49
+ loading: 'Satırlar yükleniyor…',
50
+ empty: 'Eşleşen satır yok',
51
+ retry: 'Yeniden dene',
52
+ save: 'Kaydet',
53
+ cancel: 'Vazgeç',
54
+ edit: 'Düzenle',
55
+ resize: 'Boyutlandır',
56
+ moveLeft: 'Öne taşı',
57
+ moveRight: 'Arkaya taşı',
58
+ moveUp: 'Yukarı taşı',
59
+ moveDown: 'Aşağı taşı',
60
+ pin: 'Sabitle',
61
+ unpin: 'Sabit değil',
62
+ pinLeft: 'Başa sabitle',
63
+ pinRight: 'Sona sabitle',
64
+ allRows: 'Tümü',
65
+ sortOrder: 'Sıralama önceliği',
66
+ selectionHeader: 'Seçim',
67
+ expandHeader: 'Ayrıntılar',
68
+ filter: (column) => `${column} filtrele`,
69
+ page: (page, pages, rows) => rows === undefined ? `${page} / ${pages} sayfa` : `${page} / ${pages} sayfa · ${rows} satır`,
70
+ pageNumber: (page) => `${page}. sayfa`,
71
+ selected: (count) => `${count} seçili`
72
+ };
@@ -0,0 +1,19 @@
1
+ import type { Aggregate, Column } from './types.js';
2
+ export interface PivotOptions<T> {
3
+ row: (item: T) => string;
4
+ column: (item: T) => string;
5
+ value: (item: T) => unknown;
6
+ aggregate?: Aggregate;
7
+ rowLabel?: string;
8
+ }
9
+ export interface PivotRow {
10
+ id: string;
11
+ label: string;
12
+ values: Record<string, unknown>;
13
+ total: unknown;
14
+ }
15
+ /** No workbook, chart or grid runtime dependencies are needed for a pivot. */
16
+ export declare function pivot<T>(data: readonly T[], options: PivotOptions<T>): {
17
+ data: PivotRow[];
18
+ columns: Column<PivotRow>[];
19
+ };
package/dist/pivot.js ADDED
@@ -0,0 +1,33 @@
1
+ import { aggregate } from './grouping.js';
2
+ /** No workbook, chart or grid runtime dependencies are needed for a pivot. */
3
+ export function pivot(data, options) {
4
+ const columnKeys = [...new Set(data.map(options.column))];
5
+ const buckets = new Map();
6
+ for (const item of data) {
7
+ const row = options.row(item), col = options.column(item);
8
+ const bucket = buckets.get(row) ?? new Map();
9
+ const values = bucket.get(col) ?? [];
10
+ values.push(options.value(item));
11
+ bucket.set(col, values);
12
+ buckets.set(row, bucket);
13
+ }
14
+ const op = options.aggregate ?? 'sum';
15
+ return {
16
+ data: [...buckets].map(([label, bucket]) => ({
17
+ id: label,
18
+ label,
19
+ values: Object.fromEntries(columnKeys.map((key, index) => [`p${index}`, aggregate(bucket.get(key) ?? [], op)])),
20
+ total: aggregate([...bucket.values()].flat(), op)
21
+ })),
22
+ columns: [
23
+ { id: 'label', header: options.rowLabel ?? 'Group', accessor: 'label', pin: 'left' },
24
+ ...columnKeys.map((key, index) => ({
25
+ id: `p${index}`,
26
+ header: key,
27
+ accessor: (row) => row.values[`p${index}`],
28
+ align: 'end'
29
+ })),
30
+ { id: 'total', header: 'Total', accessor: 'total', align: 'end' }
31
+ ]
32
+ };
33
+ }
@@ -0,0 +1,16 @@
1
+ import type { TableState } from './types.js';
2
+ export interface ServerResult<T> {
3
+ rows: T[];
4
+ total: number;
5
+ }
6
+ export interface ServerSnapshot<T> {
7
+ rows: T[];
8
+ total: number;
9
+ loading: boolean;
10
+ error: string | null;
11
+ }
12
+ /** Abort plus sequence guard: even a transport ignoring AbortSignal cannot overwrite newer data. */
13
+ export declare function createDataSource<T>(load: (state: TableState, signal: AbortSignal) => Promise<ServerResult<T>>, onChange: (snapshot: ServerSnapshot<T>) => void): {
14
+ request(state: TableState): Promise<void>;
15
+ dispose(): void;
16
+ };
package/dist/server.js ADDED
@@ -0,0 +1,39 @@
1
+ /** Abort plus sequence guard: even a transport ignoring AbortSignal cannot overwrite newer data. */
2
+ export function createDataSource(load, onChange) {
3
+ let controller;
4
+ let sequence = 0;
5
+ let disposed = false;
6
+ let snapshot = { rows: [], total: 0, loading: false, error: null };
7
+ const emit = (patch) => {
8
+ snapshot = { ...snapshot, ...patch };
9
+ onChange(snapshot);
10
+ };
11
+ return {
12
+ async request(state) {
13
+ if (disposed)
14
+ return;
15
+ controller?.abort();
16
+ controller = new AbortController();
17
+ const current = ++sequence;
18
+ emit({ loading: true, error: null });
19
+ try {
20
+ const result = await load(state, controller.signal);
21
+ if (current !== sequence || disposed)
22
+ return;
23
+ if (!Array.isArray(result.rows) || !Number.isFinite(result.total) || result.total < 0)
24
+ throw new Error('Invalid data source result.');
25
+ emit({ rows: result.rows, total: result.total, loading: false });
26
+ }
27
+ catch (error) {
28
+ if (current !== sequence || disposed)
29
+ return;
30
+ emit({ loading: false, error: error instanceof Error ? error.message : String(error) });
31
+ }
32
+ },
33
+ dispose() {
34
+ disposed = true;
35
+ sequence++;
36
+ controller?.abort();
37
+ }
38
+ };
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { Column, TableState } from './types.js';
2
+ export declare function serializeState(state: TableState): string;
3
+ /** Persist presentation, not data. Unknown columns and malformed values are discarded. */
4
+ export declare function restoreState<T>(serialized: string, columns: readonly Column<T>[]): TableState;
package/dist/state.js ADDED
@@ -0,0 +1,66 @@
1
+ import { defaultState, leafColumns } from './core.js';
2
+ export function serializeState(state) {
3
+ return JSON.stringify({ version: 1, state });
4
+ }
5
+ /** Persist presentation, not data. Unknown columns and malformed values are discarded. */
6
+ export function restoreState(serialized, columns) {
7
+ let raw;
8
+ try {
9
+ raw = JSON.parse(serialized);
10
+ }
11
+ catch {
12
+ return defaultState();
13
+ }
14
+ if (!raw ||
15
+ typeof raw !== 'object' ||
16
+ !('version' in raw) ||
17
+ raw.version !== 1 ||
18
+ !('state' in raw) ||
19
+ !raw.state ||
20
+ typeof raw.state !== 'object')
21
+ return defaultState();
22
+ const source = raw.state;
23
+ const known = new Set(leafColumns(columns).map((c) => c.id));
24
+ const ids = (v) => Array.isArray(v)
25
+ ? [...new Set(v.filter((id) => typeof id === 'string' && known.has(id)))]
26
+ : [];
27
+ const keys = (v) => Array.isArray(v)
28
+ ? [
29
+ ...new Set(v.filter((id) => typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id))))
30
+ ]
31
+ : [];
32
+ const state = defaultState();
33
+ state.hidden = ids(source.hidden);
34
+ state.order = ids(source.order);
35
+ state.search = typeof source.search === 'string' ? source.search : '';
36
+ for (const key of ['pageIndex', 'pageSize'])
37
+ if (typeof source[key] === 'number' && Number.isFinite(source[key]))
38
+ state[key] = Math.max(0, Math.floor(source[key]));
39
+ state.selection = keys(source.selection);
40
+ state.expanded = keys(source.expanded);
41
+ if (Array.isArray(source.sorting))
42
+ state.sorting = source.sorting.filter((s) => s && known.has(s.id) && typeof s.desc === 'boolean');
43
+ const operators = new Set([
44
+ 'contains',
45
+ 'equals',
46
+ 'startsWith',
47
+ 'gt',
48
+ 'gte',
49
+ 'lt',
50
+ 'lte',
51
+ 'between',
52
+ 'in',
53
+ 'empty'
54
+ ]);
55
+ if (Array.isArray(source.filters))
56
+ state.filters = source.filters.filter((f) => f && known.has(f.id) && (f.operator === undefined || operators.has(f.operator)));
57
+ if (source.widths && typeof source.widths === 'object')
58
+ state.widths = Object.fromEntries(Object.entries(source.widths).filter(([id, value]) => known.has(id) && typeof value === 'number' && Number.isFinite(value) && value > 0));
59
+ if (source.pinning && typeof source.pinning === 'object')
60
+ state.pinning = Object.fromEntries(Object.entries(source.pinning).filter(([id, value]) => known.has(id) && (value === false || value === 'left' || value === 'right')));
61
+ if (source.rowPinning && typeof source.rowPinning === 'object') {
62
+ const p = source.rowPinning;
63
+ state.rowPinning = { top: keys(p.top), bottom: keys(p.bottom) };
64
+ }
65
+ return state;
66
+ }