@konce-pt/table 0.8.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cell-text.d.ts +7 -0
- package/dist/cell-text.js +29 -0
- package/dist/column-filter.d.ts +30 -0
- package/dist/column-filter.js +73 -0
- package/dist/csv-export.d.ts +2 -0
- package/dist/csv-export.js +31 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/summary.d.ts +19 -0
- package/dist/summary.js +45 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 konce.pt
|
|
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,56 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI — Angular component library" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @konce-pt/table
|
|
6
|
+
|
|
7
|
+
Rdzeń tabeli danych [Koncept UI](https://gitlab.com/konce-pt/koncept-ui): filtry kolumn, agregacje
|
|
8
|
+
wiersza podsumowania, escapowanie CSV i zamiana wartości komórki na tekst. Czysty TypeScript,
|
|
9
|
+
**zero zależności runtime**, bez wiązania z frameworkiem.
|
|
10
|
+
|
|
11
|
+
Zwykle nie instalujesz go samodzielnie: używają go `KptDataTable` z `@konce-pt/angular`
|
|
12
|
+
i z `@konce-pt/react`. Sięgnij po niego wprost, gdy budujesz własną tabelę na tym samym zachowaniu
|
|
13
|
+
filtrów i podsumowań.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @konce-pt/table
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { aggregate, matchesColumnFilter, escapeCsvCell } from '@konce-pt/table';
|
|
21
|
+
|
|
22
|
+
// Filtr `select` dopasowuje też komórkę-tablicę (lista tagów albo członków zespołu).
|
|
23
|
+
matchesColumnFilter({ type: 'select', multiple: true }, ['Anna', 'Jan'], ['Jan']); // true
|
|
24
|
+
|
|
25
|
+
// Pusty zbiór: suma to 0, średnia null — komórka renderuje się pusta, nie `NaN`.
|
|
26
|
+
aggregate([], 'qty', 'sum'); // 0
|
|
27
|
+
|
|
28
|
+
// Wartość zaczynająca się od `=` byłaby w arkuszu formułą.
|
|
29
|
+
escapeCsvCell('=SUM(A1:A9)'); // "'=SUM(A1:A9)"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Wartości komórek bywają czymkolwiek
|
|
33
|
+
|
|
34
|
+
`toText()` jest jedną definicją tego, „co mówi komórka" — dla filtrowania, sortowania i podsumowań.
|
|
35
|
+
Zwykły obiekt daje pusty string, nie `'[object Object]'`: ten drugi udawałby treść, przez co filtr
|
|
36
|
+
tekstowy „obj" dopasowywałby każdy wiersz z obiektem w komórce.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
- **Tekst komórki** — `toText`
|
|
41
|
+
- **Filtry kolumn** — `matchesColumnFilter`, `isEmptyFilterValue`, typy `KptColumnFilter`,
|
|
42
|
+
`KptColumnFilterType`, `KptTableFilterOption`
|
|
43
|
+
- **Agregacje** — `aggregate`, `toNumber`, typy `KptColumnSummary`, `KptSummaryFn`
|
|
44
|
+
- **CSV** — `escapeCsvCell`
|
|
45
|
+
|
|
46
|
+
Pełny opis: [`llms.txt`](./llms.txt) (EN) i [`llms-pl.txt`](./llms-pl.txt) (PL).
|
|
47
|
+
|
|
48
|
+
## Testy
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm --filter @konce-pt/table exec node --test "src/**/*.test.ts"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Licencja
|
|
55
|
+
|
|
56
|
+
MIT © [konce.pt](https://konce.pt)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tekst z dowolnej wartości komórki. Prymitywy, `Date` i tablice zachowują się jak `String()`,
|
|
3
|
+
* natomiast zwykły obiekt daje **pusty string**, nie `'[object Object]'`: ten drugi udawałby
|
|
4
|
+
* sensowną zawartość — filtr tekstowy „obj" dopasowywałby każdy wiersz z obiektem w komórce,
|
|
5
|
+
* a sortowanie uznawało je wszystkie za równe.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toText(value: unknown): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Zamiana wartości komórki na tekst dla filtrowania, sortowania i podsumowań.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Tekst z dowolnej wartości komórki. Prymitywy, `Date` i tablice zachowują się jak `String()`,
|
|
6
|
+
* natomiast zwykły obiekt daje **pusty string**, nie `'[object Object]'`: ten drugi udawałby
|
|
7
|
+
* sensowną zawartość — filtr tekstowy „obj" dopasowywałby każdy wiersz z obiektem w komórce,
|
|
8
|
+
* a sortowanie uznawało je wszystkie za równe.
|
|
9
|
+
*/
|
|
10
|
+
export function toText(value) {
|
|
11
|
+
switch (typeof value) {
|
|
12
|
+
case 'string':
|
|
13
|
+
return value;
|
|
14
|
+
case 'number':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
case 'bigint':
|
|
17
|
+
return String(value);
|
|
18
|
+
case 'object':
|
|
19
|
+
if (value === null)
|
|
20
|
+
return '';
|
|
21
|
+
if (value instanceof Date)
|
|
22
|
+
return value.toString();
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map(toText).join(',');
|
|
25
|
+
return '';
|
|
26
|
+
default:
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opcja filtra `select`. Kształt strukturalny, więc `KptOption` z obu portów pasuje tu bez
|
|
3
|
+
* konwersji — paczka nie zna żadnego z nich.
|
|
4
|
+
*/
|
|
5
|
+
export interface KptTableFilterOption {
|
|
6
|
+
label: string;
|
|
7
|
+
value: unknown;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Rodzaj kontrolki filtra w nagłówku kolumny. */
|
|
11
|
+
export type KptColumnFilterType = 'text' | 'number' | 'select' | 'dateRange' | 'numberRange' | 'slider';
|
|
12
|
+
export interface KptColumnFilter {
|
|
13
|
+
type: KptColumnFilterType;
|
|
14
|
+
/** Opcje dla `type: 'select'`. */
|
|
15
|
+
options?: KptTableFilterOption[];
|
|
16
|
+
/** Wielokrotny wybór dla `type: 'select'`. */
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
/** Placeholder kontrolki. */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Dolna granica dla `type: 'slider'` (domyślnie 0). */
|
|
21
|
+
min?: number;
|
|
22
|
+
/** Górna granica dla `type: 'slider'` (domyślnie 100). */
|
|
23
|
+
max?: number;
|
|
24
|
+
/** Krok dla `type: 'slider'` (domyślnie 1). */
|
|
25
|
+
step?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Filtr „pusty" (nie wybrano nic) przepuszcza każdy wiersz. */
|
|
28
|
+
export declare function isEmptyFilterValue(value: unknown): boolean;
|
|
29
|
+
/** Czy komórka przechodzi filtr kolumny. Wartości pustych filtrów odsiewa `isEmptyFilterValue`. */
|
|
30
|
+
export declare function matchesColumnFilter(filter: KptColumnFilter, cell: unknown, value: unknown): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dopasowanie wiersza do filtrów kolumn tabeli.
|
|
3
|
+
* Komponent trzyma tylko stan filtrów; cała semantyka „czy komórka pasuje" jest tutaj.
|
|
4
|
+
*/
|
|
5
|
+
import { toText } from "./cell-text.js";
|
|
6
|
+
import { toNumber } from "./summary.js";
|
|
7
|
+
/** Data komórki w formacie `YYYY-MM-DD` — do porównań leksykograficznych z granicami zakresu. */
|
|
8
|
+
function toIsoDate(value) {
|
|
9
|
+
if (value instanceof Date) {
|
|
10
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
11
|
+
return `${value.getFullYear()}-${p(value.getMonth() + 1)}-${p(value.getDate())}`;
|
|
12
|
+
}
|
|
13
|
+
return toText(value);
|
|
14
|
+
}
|
|
15
|
+
/** Filtr „pusty" (nie wybrano nic) przepuszcza każdy wiersz. */
|
|
16
|
+
export function isEmptyFilterValue(value) {
|
|
17
|
+
return value == null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
18
|
+
}
|
|
19
|
+
const matchesText = (cell, value) => toText(cell).toLowerCase().includes(toText(value).toLowerCase());
|
|
20
|
+
const matchesNumber = (cell, value) => toText(cell).includes(toText(value));
|
|
21
|
+
const matchesSelect = (cell, value) => {
|
|
22
|
+
const picked = Array.isArray(value) ? value : [value];
|
|
23
|
+
// Komórka-tablica (np. lista tagów/członków) → dopasuj, gdy zawiera dowolną z wybranych wartości.
|
|
24
|
+
if (Array.isArray(cell))
|
|
25
|
+
return cell.some((c) => picked.includes(c));
|
|
26
|
+
return picked.includes(cell);
|
|
27
|
+
};
|
|
28
|
+
const matchesNumberRange = (cell, value) => {
|
|
29
|
+
const { min, max } = value ?? {};
|
|
30
|
+
const lo = min == null || min === '' ? null : Number.parseFloat(min);
|
|
31
|
+
const hi = max == null || max === '' ? null : Number.parseFloat(max);
|
|
32
|
+
if (lo === null && hi === null)
|
|
33
|
+
return true;
|
|
34
|
+
const num = toNumber(cell);
|
|
35
|
+
if (Number.isNaN(num))
|
|
36
|
+
return false;
|
|
37
|
+
if (lo !== null && num < lo)
|
|
38
|
+
return false;
|
|
39
|
+
if (hi !== null && num > hi)
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
const matchesSlider = (cell, value) => {
|
|
44
|
+
const [lo, hi] = value;
|
|
45
|
+
const num = toNumber(cell);
|
|
46
|
+
if (Number.isNaN(num))
|
|
47
|
+
return false;
|
|
48
|
+
return num >= lo && num <= hi;
|
|
49
|
+
};
|
|
50
|
+
const matchesDateRange = (cell, value) => {
|
|
51
|
+
const { start, end } = value ?? {};
|
|
52
|
+
if (!start && !end)
|
|
53
|
+
return true;
|
|
54
|
+
const iso = toIsoDate(cell);
|
|
55
|
+
if (start && iso < start)
|
|
56
|
+
return false;
|
|
57
|
+
if (end && iso > end)
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
/** Mapa zamiast `switch` — `Record` po unii typów wymusza komplet, więc nie ma martwego `default`. */
|
|
62
|
+
const MATCHERS = {
|
|
63
|
+
text: matchesText,
|
|
64
|
+
number: matchesNumber,
|
|
65
|
+
select: matchesSelect,
|
|
66
|
+
numberRange: matchesNumberRange,
|
|
67
|
+
slider: matchesSlider,
|
|
68
|
+
dateRange: matchesDateRange,
|
|
69
|
+
};
|
|
70
|
+
/** Czy komórka przechodzi filtr kolumny. Wartości pustych filtrów odsiewa `isEmptyFilterValue`. */
|
|
71
|
+
export function matchesColumnFilter(filter, cell, value) {
|
|
72
|
+
return MATCHERS[filter.type](cell, value);
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Escapowanie komórek CSV dla eksportu tabeli.
|
|
3
|
+
*/
|
|
4
|
+
// Znak startowy, który arkusz kalkulacyjny czyta jako początek formuły (CWE-1236).
|
|
5
|
+
const FORMULA_PREFIX = /^[=+\-@\t\r]/;
|
|
6
|
+
function stringifyCsvValue(value) {
|
|
7
|
+
switch (typeof value) {
|
|
8
|
+
case 'undefined':
|
|
9
|
+
return '';
|
|
10
|
+
case 'string':
|
|
11
|
+
return value;
|
|
12
|
+
case 'number':
|
|
13
|
+
case 'boolean':
|
|
14
|
+
case 'bigint':
|
|
15
|
+
return String(value);
|
|
16
|
+
default:
|
|
17
|
+
if (value === null)
|
|
18
|
+
return '';
|
|
19
|
+
if (value instanceof Date)
|
|
20
|
+
return value.toISOString();
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Escapuje pojedynczą wartość komórki/nagłówka do bezpiecznego pola CSV (separator `;`). */
|
|
25
|
+
export function escapeCsvCell(value) {
|
|
26
|
+
let text = stringifyCsvValue(value);
|
|
27
|
+
// Wiodący `'` każe arkuszowi traktować komórkę jako tekst — sam znak nie jest wyświetlany.
|
|
28
|
+
if (FORMULA_PREFIX.test(text))
|
|
29
|
+
text = `'${text}`;
|
|
30
|
+
return /[",\n;]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { toText } from './cell-text.ts';
|
|
2
|
+
export { escapeCsvCell } from './csv-export.ts';
|
|
3
|
+
export { aggregate, toNumber } from './summary.ts';
|
|
4
|
+
export type { KptColumnSummary, KptSummaryFn } from './summary.ts';
|
|
5
|
+
export { isEmptyFilterValue, matchesColumnFilter } from './column-filter.ts';
|
|
6
|
+
export type { KptColumnFilter, KptColumnFilterType, KptTableFilterOption } from './column-filter.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Wbudowana agregacja kolumny w wierszu podsumowania. */
|
|
2
|
+
export type KptSummaryFn = 'sum' | 'avg' | 'min' | 'max' | 'count';
|
|
3
|
+
export interface KptColumnSummary {
|
|
4
|
+
/**
|
|
5
|
+
* Agregacja wbudowana albo własna funkcja liczona na całym zbiorze wierszy
|
|
6
|
+
* (po filtrach, ze wszystkich stron).
|
|
7
|
+
*/
|
|
8
|
+
fn: KptSummaryFn | ((rows: readonly Record<string, unknown>[]) => unknown);
|
|
9
|
+
/** Formatowanie wyniku (np. waluta). Bez niego wartość renderowana jest surowo. */
|
|
10
|
+
format?: (value: unknown) => string;
|
|
11
|
+
}
|
|
12
|
+
/** Liczba z komórki: `number` bez zmian, string przez `parseFloat`, reszta → `NaN` (pomijana). */
|
|
13
|
+
export declare function toNumber(value: unknown): number;
|
|
14
|
+
/**
|
|
15
|
+
* Agreguje jedno pole po wierszach. `count` liczy wiersze; pozostałe funkcje pomijają
|
|
16
|
+
* wartości nieliczbowe. Pusty zbiór liczb: `sum` → 0, `avg`/`min`/`max` → `null`
|
|
17
|
+
* (komórka renderuje się wtedy pusta).
|
|
18
|
+
*/
|
|
19
|
+
export declare function aggregate(rows: readonly Record<string, unknown>[], field: string, fn: KptSummaryFn): number | null;
|
package/dist/summary.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Agregacje wiersza podsumowania (`<tfoot>`) tabeli danych.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Sumowanie liczb zmiennoprzecinkowych daje ogony (`0.1 + 0.2` → `0.30000000000000004`),
|
|
6
|
+
* które bez tego przycięcia wyciekłyby wprost do komórki podsumowania.
|
|
7
|
+
*/
|
|
8
|
+
function trimFloat(value) {
|
|
9
|
+
return Math.round(value * 1e10) / 1e10;
|
|
10
|
+
}
|
|
11
|
+
/** Liczba z komórki: `number` bez zmian, string przez `parseFloat`, reszta → `NaN` (pomijana). */
|
|
12
|
+
export function toNumber(value) {
|
|
13
|
+
if (typeof value === 'number')
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
16
|
+
return Number.parseFloat(value);
|
|
17
|
+
return Number.NaN;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Agreguje jedno pole po wierszach. `count` liczy wiersze; pozostałe funkcje pomijają
|
|
21
|
+
* wartości nieliczbowe. Pusty zbiór liczb: `sum` → 0, `avg`/`min`/`max` → `null`
|
|
22
|
+
* (komórka renderuje się wtedy pusta).
|
|
23
|
+
*/
|
|
24
|
+
export function aggregate(rows, field, fn) {
|
|
25
|
+
if (fn === 'count')
|
|
26
|
+
return rows.length;
|
|
27
|
+
const nums = [];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const num = toNumber(row[field]);
|
|
30
|
+
if (!Number.isNaN(num))
|
|
31
|
+
nums.push(num);
|
|
32
|
+
}
|
|
33
|
+
switch (fn) {
|
|
34
|
+
case 'sum':
|
|
35
|
+
return trimFloat(nums.reduce((acc, n) => acc + n, 0));
|
|
36
|
+
case 'avg':
|
|
37
|
+
return nums.length === 0 ? null : trimFloat(nums.reduce((acc, n) => acc + n, 0) / nums.length);
|
|
38
|
+
case 'min':
|
|
39
|
+
return nums.length === 0 ? null : Math.min(...nums);
|
|
40
|
+
case 'max':
|
|
41
|
+
return nums.length === 0 ? null : Math.max(...nums);
|
|
42
|
+
default:
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@konce-pt/table",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Data-table core for Koncept UI — column filters, aggregations, CSV escaping, cell-to-text. Framework-free TypeScript, zero runtime dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "konce.pt",
|
|
7
|
+
"homepage": "https://ui.konce.pt/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://gitlab.com/konce-pt/koncept-ui.git",
|
|
11
|
+
"directory": "packages/table"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://gitlab.com/konce-pt/koncept-ui/-/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"data-table",
|
|
18
|
+
"table",
|
|
19
|
+
"filtering",
|
|
20
|
+
"design-system",
|
|
21
|
+
"koncept-ui",
|
|
22
|
+
"kpt"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "~6.0.3"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
43
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
|
|
44
|
+
}
|
|
45
|
+
}
|