@iyulab/flex-table 0.40.0 → 0.40.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/CHANGELOG.md +40 -0
- package/README.md +6 -6
- package/dist/clipboard/clipboard.d.ts +2 -6
- package/dist/{flex-table-CDEu8YQE.js → flex-table-CbIsTvod.js} +221 -933
- package/dist/flex-table.js +1 -1
- package/dist/react-events.test.d.ts +1 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +9 -2
- package/package.json +3 -2
- package/skills/iyulab-flex-table/SKILL.md +135 -0
- package/skills/iyulab-flex-table/references/api.md +173 -0
- package/skills/iyulab-flex-table/references/react.md +177 -0
- package/skills/iyulab-flex-table/references/styling.md +108 -0
package/dist/flex-table.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-CbIsTvod.js";
|
|
2
2
|
export { o as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, r as flexTableLocale, a as renderCell };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/react.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ declare const FlexTableReactBase: import("@lit/react").ReactWebComponent<FlexTab
|
|
|
27
27
|
onBatchUpdate: EventName<CustomEvent>;
|
|
28
28
|
onContextMenu: EventName<CustomEvent>;
|
|
29
29
|
onFilterError: EventName<CustomEvent>;
|
|
30
|
+
onRowReorder: EventName<CustomEvent>;
|
|
31
|
+
onColumnVisibilityChange: EventName<CustomEvent>;
|
|
32
|
+
onCommentChange: EventName<CustomEvent>;
|
|
33
|
+
onDataImport: EventName<CustomEvent>;
|
|
34
|
+
onFillHandleApply: EventName<CustomEvent>;
|
|
35
|
+
onFindReplace: EventName<CustomEvent>;
|
|
36
|
+
onHeaderContextMenu: EventName<CustomEvent>;
|
|
30
37
|
}>;
|
|
31
38
|
type BaseProps = React.ComponentProps<typeof FlexTableReactBase>;
|
|
32
39
|
/**
|
package/dist/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as e } from "./flex-table-
|
|
1
|
+
import { t as e } from "./flex-table-CbIsTvod.js";
|
|
2
2
|
import { i as t, t as n } from "./query-Dw4iO0ir.js";
|
|
3
3
|
import { t as r } from "./view-DiLr-TdY.js";
|
|
4
4
|
import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
|
|
@@ -165,7 +165,14 @@ var p = u({
|
|
|
165
165
|
onValidationError: "validation-error",
|
|
166
166
|
onBatchUpdate: "batch-update",
|
|
167
167
|
onContextMenu: "context-menu",
|
|
168
|
-
onFilterError: "filter-error"
|
|
168
|
+
onFilterError: "filter-error",
|
|
169
|
+
onRowReorder: "row-reorder",
|
|
170
|
+
onColumnVisibilityChange: "column-visibility-change",
|
|
171
|
+
onCommentChange: "comment-change",
|
|
172
|
+
onDataImport: "data-import",
|
|
173
|
+
onFillHandleApply: "fill-handle-apply",
|
|
174
|
+
onFindReplace: "find-replace",
|
|
175
|
+
onHeaderContextMenu: "header-context-menu"
|
|
169
176
|
}
|
|
170
177
|
});
|
|
171
178
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/flex-table",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"description": "A minimalist, input-centric data grid web component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/flex-table.js",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
|
+
"skills",
|
|
36
37
|
"package.json",
|
|
37
38
|
"README.md",
|
|
38
39
|
"CHANGELOG.md",
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
"odata-query": "^8.1.0"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|
|
74
|
-
"@iyulab/components": ">=1.
|
|
75
|
+
"@iyulab/components": ">=1.52.0",
|
|
75
76
|
"@lit/react": "^1.0.0",
|
|
76
77
|
"react": "^18.0.0 || ^19.0.0"
|
|
77
78
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: iyulab-flex-table
|
|
3
|
+
description: Spreadsheet-grade data grid web component (`<flex-table>`, built with Lit) with virtual scrolling, inline cell editing, validation, range selection, clipboard, undo/redo, sorting, filtering, pinned columns, and CSV/TSV/JSON/XLSX export, plus a React wrapper and OData / in-memory data-source hooks. Use when working with @iyulab/flex-table — defining columns, editing cells, handling grid events, wiring server-side paging with useODataSource or useArraySource, or styling the grid.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: iyulab
|
|
7
|
+
version: "0.40.1"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# @iyulab/flex-table
|
|
11
|
+
|
|
12
|
+
A schema-agnostic data grid custom element (`<flex-table>`) for large datasets and cell-level editing.
|
|
13
|
+
Rows are plain objects (`DataRow = Record<string, unknown>`); columns describe how to show and edit them.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @iyulab/flex-table @iyulab/components
|
|
19
|
+
# React wrapper and hooks (optional peers)
|
|
20
|
+
npm install @lit/react react
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`@iyulab/components` is a peer dependency (locale, formatting, design tokens).
|
|
24
|
+
|
|
25
|
+
## Entry points
|
|
26
|
+
|
|
27
|
+
| Import | Contents |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `@iyulab/flex-table` | Registers `<flex-table>`; exports `FlexTable`, types, `exportData`, `renderCell`, `flexTableLocale`, `RowSelectionState`, `UndoStack`, `effectiveAlign` |
|
|
30
|
+
| `@iyulab/flex-table/react` | `FlexTableReact`, `useODataSource`, `useArraySource` |
|
|
31
|
+
| `@iyulab/flex-table/odata` | Pure helpers, no React: `buildODataQuery`, `buildSearchExpression`, `parseOrderBy` |
|
|
32
|
+
| `@iyulab/flex-table/array` | Pure helper, no React: `computeArrayView` |
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<flex-table id="table" style="height: 400px" show-row-numbers></flex-table>
|
|
38
|
+
|
|
39
|
+
<script type="module">
|
|
40
|
+
import '@iyulab/flex-table';
|
|
41
|
+
|
|
42
|
+
const table = document.getElementById('table');
|
|
43
|
+
table.columns = [
|
|
44
|
+
{ key: 'name', label: 'Name', type: 'text', width: 200 },
|
|
45
|
+
{ key: 'age', label: 'Age', type: 'number', width: 100 },
|
|
46
|
+
{ key: 'active', label: 'Active', type: 'boolean', width: 80 },
|
|
47
|
+
];
|
|
48
|
+
table.data = [
|
|
49
|
+
{ name: 'Alice', age: 30, active: true },
|
|
50
|
+
{ name: 'Bob', age: 25, active: false },
|
|
51
|
+
];
|
|
52
|
+
table.addEventListener('cell-edit-commit', (e) => console.log(e.detail));
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Key concepts
|
|
57
|
+
|
|
58
|
+
**Give it a height.** The host is its own scroll container and virtualizes against its own height.
|
|
59
|
+
Without a height (or a constrained flex parent with `min-height: 0`), every row is rendered.
|
|
60
|
+
|
|
61
|
+
**Columns.** Each `ColumnDefinition` needs `key` and `label`. `type` picks the built-in
|
|
62
|
+
renderer/editor (`text`, `number`, `boolean`, `date`, `datetime`, `select`; unknown strings behave
|
|
63
|
+
as `text`). See `references/api.md` for every field.
|
|
64
|
+
|
|
65
|
+
- `format` changes only the displayed text (`'#,##0.00'`, `'0.00%'`, `'yyyy-MM-dd'`, or a function); sorting, filtering, editing and export keep the raw value.
|
|
66
|
+
- `render` replaces the cell content (`(value, row, col) => TemplateResult | string`) and wins over `format`.
|
|
67
|
+
- `editor` returns a Lit template containing an element with class `ft-editor`; its `.value` is committed.
|
|
68
|
+
- `validator` returns `null` when valid, or an error message (the edit is rejected and `validation-error` fires).
|
|
69
|
+
- `conditionalRules` applies `{ when, style }` rules in order; later matches override earlier ones.
|
|
70
|
+
- `pinned: 'left' | 'right'` freezes a column during horizontal scroll.
|
|
71
|
+
|
|
72
|
+
**Data is mutated in place.** Assigning a new array re-renders; changing a row object does not.
|
|
73
|
+
Use `updateRows([{ row, key, value }])` for programmatic edits (undoable, fires `batch-update`) or
|
|
74
|
+
`refreshData()` after an external in-place mutation.
|
|
75
|
+
|
|
76
|
+
**Editing and read-only.** `editable` defaults to `true`. For a read-only grid set
|
|
77
|
+
`table.editable = false` (a property, since a boolean attribute cannot express `false`) — Enter on a non-editable cell then fires
|
|
78
|
+
`row-activate`, the grid's "open this row" contract.
|
|
79
|
+
|
|
80
|
+
**Row selection is index-based.** Enable with `selectable` (`selection-mode="single|multi"`).
|
|
81
|
+
There is no row key, so set `clear-selection-on-data-change` when selection drives bulk actions and
|
|
82
|
+
`data` can be replaced. `selectWhere(predicate)` selects rows by content.
|
|
83
|
+
|
|
84
|
+
**Client vs server mode.** `data-mode="client"` (default) sorts and filters locally.
|
|
85
|
+
`data-mode="server"` only emits `sort-change` / `filter-change`; you supply already-processed rows.
|
|
86
|
+
|
|
87
|
+
**Filtering.** Programmatic `setFilter(key, (value, row) => boolean)`; `show-filters` adds a
|
|
88
|
+
**Filter…** entry to each column's header menu (text, number range, boolean, date/datetime range).
|
|
89
|
+
Both share one filter state.
|
|
90
|
+
|
|
91
|
+
**Undo/redo** covers edits, row and column operations, paste and comments (`max-undo-size`, default 100).
|
|
92
|
+
|
|
93
|
+
## React
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { useRef } from 'react';
|
|
97
|
+
import { FlexTableReact, type FlexTable, type ColumnDefinition } from '@iyulab/flex-table/react';
|
|
98
|
+
|
|
99
|
+
interface Order { id: string; total: number; currency: string }
|
|
100
|
+
|
|
101
|
+
const columns: ColumnDefinition<Order>[] = [
|
|
102
|
+
{ key: 'id', label: 'ID' },
|
|
103
|
+
{ key: 'total', label: 'Total', render: (_v, row) => `${row.total} ${row.currency}` },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
function Orders({ orders }: { orders: Order[] }) {
|
|
107
|
+
const ref = useRef<FlexTable>(null);
|
|
108
|
+
return (
|
|
109
|
+
<FlexTableReact<Order>
|
|
110
|
+
ref={ref}
|
|
111
|
+
data={orders}
|
|
112
|
+
columns={columns}
|
|
113
|
+
selectable
|
|
114
|
+
onCellEditCommit={(e) => console.log(e.detail)}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Server paging from OData or an in-memory array uses the same binding — see `references/react.md`.
|
|
121
|
+
|
|
122
|
+
## Common pitfalls
|
|
123
|
+
|
|
124
|
+
- No height → no virtualization.
|
|
125
|
+
- `--ft-row-height` is read once at first render; change row height later via `rowHeight` / `row-height`.
|
|
126
|
+
- Styles for elements returned by `render` must be passed through the `stylesheets` property
|
|
127
|
+
(constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
|
|
128
|
+
- `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
|
|
129
|
+
- Always render `error` from `useODataSource`; a failed request otherwise leaves the grid empty.
|
|
130
|
+
|
|
131
|
+
## References
|
|
132
|
+
|
|
133
|
+
- [references/api.md](references/api.md) — properties, column definition, methods, events
|
|
134
|
+
- [references/react.md](references/react.md) — `FlexTableReact`, `useODataSource`, `useArraySource`, pure OData/array helpers
|
|
135
|
+
- [references/styling.md](references/styling.md) — CSS custom properties, density, keyboard shortcuts, localization
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# flex-table API reference
|
|
2
|
+
|
|
3
|
+
All names below are members of the `FlexTable` class (`<flex-table>`).
|
|
4
|
+
|
|
5
|
+
## Properties
|
|
6
|
+
|
|
7
|
+
| Property | Attribute | Type | Default | Notes |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| `columns` | — | `ColumnDefinition[]` | `[]` | Column definitions |
|
|
10
|
+
| `data` | — | `DataRow[]` | `[]` | Rows; mutated in place by the grid |
|
|
11
|
+
| `rowHeight` | `row-height` | `number` | `32` | Explicit value > `--ft-row-height` token > 32 |
|
|
12
|
+
| `showRowNumbers` | `show-row-numbers` | `boolean` | `false` | Sticky row-number column |
|
|
13
|
+
| `theme` | `theme` | `'light' \| 'dark' \| undefined` | auto | Auto follows `prefers-color-scheme` |
|
|
14
|
+
| `editable` | `editable` | `boolean` | `true` | `false` = read-only grid; Enter then fires `row-activate` |
|
|
15
|
+
| `showFilters` | `show-filters` | `boolean` | `false` | Adds **Filter…** to each column menu |
|
|
16
|
+
| `showContextMenu` | `show-context-menu` | `boolean` | `false` | Built-in cell context menu |
|
|
17
|
+
| `frozenRows` | `frozen-rows` | `number` | `0` | Rows kept visible at the top |
|
|
18
|
+
| `maxRows` | `max-rows` | `number` | `0` | 0 = unlimited; blocks `addRow()` and paste expansion |
|
|
19
|
+
| `maxUndoSize` | `max-undo-size` | `number` | `100` | Undo stack size |
|
|
20
|
+
| `selectable` | `selectable` | `boolean` | `false` | Row checkbox selection |
|
|
21
|
+
| `selectionMode` | `selection-mode` | `'single' \| 'multi'` | `'multi'` | |
|
|
22
|
+
| `dataMode` | `data-mode` | `'client' \| 'server'` | `'client'` | Server mode only emits sort/filter events |
|
|
23
|
+
| `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Summary row keyed by column key |
|
|
24
|
+
| `emptyMessage` | `empty-message` | `string` | `'No data'` | |
|
|
25
|
+
| `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | All rows hidden by filters |
|
|
26
|
+
| `loading` | `loading` | `boolean` | `false` | Loading overlay + `aria-busy` |
|
|
27
|
+
| `importEnabled` | `import-enabled` | `boolean` | `false` | Drag-and-drop `.xlsx` / `.csv` import |
|
|
28
|
+
| `clearUndoOnDataChange` | `clear-undo-on-data-change` | `boolean` | `false` | Replacing `data` clears undo history |
|
|
29
|
+
| `clearSelectionOnDataChange` | `clear-selection-on-data-change` | `boolean` | `false` | Replacing `data` clears row selection |
|
|
30
|
+
| `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Adopted into the shadow root; styles `render` output |
|
|
31
|
+
|
|
32
|
+
Replacing `data` with the same array reference does not trigger the clear-on-change behaviors.
|
|
33
|
+
|
|
34
|
+
### Read-only
|
|
35
|
+
|
|
36
|
+
| Getter | Type |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `visibleColumns` | `ColumnDefinition[]` (non-hidden) |
|
|
39
|
+
| `filteredRowCount` | `number` |
|
|
40
|
+
| `filterKeys` | `string[]` |
|
|
41
|
+
| `sortCriteria` | `SortCriteria[]` — `{ key, direction: 'asc' \| 'desc' }` |
|
|
42
|
+
| `activeCell` / `editingCell` | `CellPosition \| null` — `{ row, col }` |
|
|
43
|
+
| `canUndo` / `canRedo` | `boolean` |
|
|
44
|
+
|
|
45
|
+
## Column definition
|
|
46
|
+
|
|
47
|
+
`ColumnDefinition<T = DataRow>` fields:
|
|
48
|
+
|
|
49
|
+
| Field | Type | Notes |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `key` | `string` | Property name in the row (required) |
|
|
52
|
+
| `label` | `string` | Header text (required) |
|
|
53
|
+
| `type` | `ColumnType` | `'text' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'select'`; other strings act as text |
|
|
54
|
+
| `width` / `minWidth` | `number` | px; `minWidth` defaults to 40 |
|
|
55
|
+
| `hidden` | `boolean` | |
|
|
56
|
+
| `sortable` | `boolean` | default `true` |
|
|
57
|
+
| `align` | `'start' \| 'center' \| 'end'` | Default from type: number → end, boolean → center, else start |
|
|
58
|
+
| `headerAlign` | `'start' \| 'center' \| 'end'` | Defaults to the cell alignment |
|
|
59
|
+
| `editable` | `boolean` | Per-column; the global `editable` still applies |
|
|
60
|
+
| `pinned` | `'left' \| 'right'` | |
|
|
61
|
+
| `options` | `string[] \| SelectOption[]` | For `select`; `SelectOption` = `{ label, value }` |
|
|
62
|
+
| `autocomplete` | `boolean \| 'strict'` | Suggest existing values; `'strict'` rejects others |
|
|
63
|
+
| `format` | `string \| (value, row, col) => string` | Display only |
|
|
64
|
+
| `render` | `CellRenderer<T>` | `(value, row, col) => TemplateResult \| string` |
|
|
65
|
+
| `editor` | `CellEditor<T>` | `(value, row, col) => TemplateResult` with an `.ft-editor` element |
|
|
66
|
+
| `validator` | `CellValidator<T>` | `(value, row, col) => string \| null \| undefined` |
|
|
67
|
+
| `conditionalRules` | `ConditionalRule<T>[]` | `{ when(value, row, col): boolean, style: CellStyle }` |
|
|
68
|
+
|
|
69
|
+
`CellStyle` = `{ background?, color?, fontWeight?: 'bold' | 'normal', fontStyle?: 'italic' | 'normal' }`.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { html } from 'lit';
|
|
73
|
+
|
|
74
|
+
table.columns = [
|
|
75
|
+
{ key: 'total', label: 'Total', type: 'number', format: '#,##0.00' },
|
|
76
|
+
{ key: 'status', label: 'Status',
|
|
77
|
+
conditionalRules: [{ when: (v) => v === 'overdue', style: { color: '#dc2626', fontWeight: 'bold' } }] },
|
|
78
|
+
{ key: 'age', label: 'Age', type: 'number',
|
|
79
|
+
validator: (v) => (Number(v) < 0 ? 'Must be positive' : null) },
|
|
80
|
+
{ key: 'color', label: 'Color',
|
|
81
|
+
editor: (v) => html`<input class="ft-editor" type="color" .value=${String(v ?? '#000000')}>` },
|
|
82
|
+
];
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Custom editors: clicking another cell auto-commits; handle `@keydown` / `@blur` in the template for
|
|
86
|
+
Enter/Escape and blur-to-commit behavior.
|
|
87
|
+
|
|
88
|
+
## Methods
|
|
89
|
+
|
|
90
|
+
### Rows
|
|
91
|
+
|
|
92
|
+
| Method | Returns | Notes |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `addRow(row?, index?)` | `DataRow \| null` | `null` when `maxRows` reached |
|
|
95
|
+
| `deleteRows(indices?)` | `void` | Data indices; default = selected rows |
|
|
96
|
+
| `updateRows(changes)` | `void` | `Array<{ row, key, value }>`, one undo step |
|
|
97
|
+
| `refreshData()` | `void` | Re-render after in-place mutation |
|
|
98
|
+
|
|
99
|
+
### Columns
|
|
100
|
+
|
|
101
|
+
| Method | Returns |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `addColumn(def, index?)` | `ColumnDefinition` |
|
|
104
|
+
| `deleteColumn(key)` | `void` (also clears its filter, sort and width) |
|
|
105
|
+
| `moveColumn(key, newIndex)` | `void` (index clamped) |
|
|
106
|
+
| `hideColumn(key)` / `showColumn(key)` | `void` |
|
|
107
|
+
| `getHiddenColumns()` | `ColumnDefinition[]` |
|
|
108
|
+
| `getColumnWidth(key)` | `number \| undefined` (resized width) |
|
|
109
|
+
| `selectColumn(colIndex)` | `void` (range-selects the column) |
|
|
110
|
+
|
|
111
|
+
### Row selection
|
|
112
|
+
|
|
113
|
+
| Method | Notes |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `selectAll()` / `deselectAll()` | `selectAll` is multi mode only |
|
|
116
|
+
| `selectWhere(predicate)` | `(row, dataIndex) => boolean` over the visible rows |
|
|
117
|
+
| `getSelectedRows()` | `{ selectedIndices, selectedRows }` |
|
|
118
|
+
|
|
119
|
+
### Filtering, undo, comments, import/export
|
|
120
|
+
|
|
121
|
+
| Method | Notes |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `setFilter(key, predicate)` / `removeFilter(key)` / `clearFilters()` | `predicate: (value, row) => boolean`; filters combine with AND |
|
|
124
|
+
| `undo()` / `redo()` / `clearUndoHistory()` | |
|
|
125
|
+
| `setComment(dataIndex, colKey, text)` | `null` or `''` removes; undoable |
|
|
126
|
+
| `getComment(dataIndex, colKey)` / `clearComments()` | |
|
|
127
|
+
| `importFromFile(file)` | `Promise<void>`; `.xlsx`, `.csv`, `.tsv`; fires `data-import` |
|
|
128
|
+
| `exportToString(format, { selectionOnly? })` | `string \| Uint8Array`; format `'csv' \| 'tsv' \| 'json' \| 'xlsx'` (xlsx returns bytes) |
|
|
129
|
+
| `exportToFile(format, filename?)` | Triggers a download |
|
|
130
|
+
|
|
131
|
+
## Events
|
|
132
|
+
|
|
133
|
+
All are `CustomEvent`s with `bubbles: true, composed: true`. `row`/`index` are data indices.
|
|
134
|
+
|
|
135
|
+
| Event | `detail` |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `cell-select` | `{ row, col }` (or `null`) |
|
|
138
|
+
| `cell-edit-start` | `{ row, col, key, value }` |
|
|
139
|
+
| `cell-edit-commit` | `{ row, col, key, oldValue, newValue }` |
|
|
140
|
+
| `cell-edit-cancel` | `{ row, col }` |
|
|
141
|
+
| `validation-error` | `{ row, col, key, value, error }` |
|
|
142
|
+
| `row-add` | `{ row, index }` |
|
|
143
|
+
| `row-delete` | `{ indices, rows }` |
|
|
144
|
+
| `row-activate` | `{ row, index, col, key }` — Enter on a non-editable cell |
|
|
145
|
+
| `row-reorder` | `{ from, to }` |
|
|
146
|
+
| `batch-update` | `{ changes }` |
|
|
147
|
+
| `column-add` | `{ column, index }` |
|
|
148
|
+
| `column-delete` | `{ column, key, index }` |
|
|
149
|
+
| `column-reorder` | `{ key, oldIndex, newIndex }` |
|
|
150
|
+
| `column-resize` | `{ key, width, colIndex }` |
|
|
151
|
+
| `column-select` | `{ colIndex, key, rowCount }` |
|
|
152
|
+
| `column-visibility-change` | `{ key, hidden }` |
|
|
153
|
+
| `sort-change` | `{ criteria }` |
|
|
154
|
+
| `filter-change` | `{ keys, filteredCount }` |
|
|
155
|
+
| `filter-error` | `{ error, row, filterKey }` |
|
|
156
|
+
| `selection-change` | `{ selectedIndices, selectedRows }` |
|
|
157
|
+
| `clipboard-copy` / `clipboard-cut` | `{ range, text }` (TSV) |
|
|
158
|
+
| `clipboard-paste` | `{ changes, addedRows }` |
|
|
159
|
+
| `clipboard-error` | `{ action: 'copy' \| 'paste', error }` |
|
|
160
|
+
| `fill-handle-apply` | `{ sourceRange, targetRange, cells }` |
|
|
161
|
+
| `find-replace` | `{ type: 'replace' \| 'replace-all', cells }` |
|
|
162
|
+
| `comment-change` | `{ dataIndex, colKey, text }` |
|
|
163
|
+
| `data-import` | `{ count }` |
|
|
164
|
+
| `undo-state-change` | `{ canUndo, canRedo }` |
|
|
165
|
+
| `context-menu` | `{ x, y, row, col, key, value, rowData }` — cancelable; `preventDefault()` suppresses the built-in menu |
|
|
166
|
+
| `header-context-menu` | `{ key, label, x, y }` |
|
|
167
|
+
|
|
168
|
+
## Other exports
|
|
169
|
+
|
|
170
|
+
`exportData(rows, columns, format)`, `renderCell`, `effectiveAlign(col)`, `RowSelectionState`,
|
|
171
|
+
`UndoStack`, `flexTableLocale`, and the types `CellPosition`, `CellRange`, `SortCriteria`,
|
|
172
|
+
`SortDirection`, `ColumnFilter`, `FilterPredicate`, `FilterErrorCallback`, `ExportFormat`,
|
|
173
|
+
`UndoAction`, `FlexTableMessageKey`.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# React: `@iyulab/flex-table/react`
|
|
2
|
+
|
|
3
|
+
Requires the optional peers `react` and `@lit/react`.
|
|
4
|
+
|
|
5
|
+
## `FlexTableReact`
|
|
6
|
+
|
|
7
|
+
A `@lit/react` wrapper around `<flex-table>`, generic over the row type `T` (default `DataRow`).
|
|
8
|
+
Every element property is a prop; `ref` resolves to the `FlexTable` instance, so all methods
|
|
9
|
+
(`addRow`, `deleteRows`, `setFilter`, `selectWhere`, …) are reachable imperatively.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { useRef } from 'react';
|
|
13
|
+
import { FlexTableReact, type FlexTable, type ColumnDefinition } from '@iyulab/flex-table/react';
|
|
14
|
+
|
|
15
|
+
const columns: ColumnDefinition<Order>[] = [
|
|
16
|
+
{ key: 'id', label: 'ID' },
|
|
17
|
+
{ key: 'total', label: 'Total', type: 'number', format: '#,##0.00' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function Orders({ orders }: { orders: Order[] }) {
|
|
21
|
+
const ref = useRef<FlexTable>(null);
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<button onClick={() => ref.current?.addRow({ id: '', total: 0 })}>Add</button>
|
|
25
|
+
<FlexTableReact<Order> ref={ref} data={orders} columns={columns} selectable
|
|
26
|
+
onSelectionChange={(e) => console.log(e.detail.selectedRows)} />
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Mapped event props
|
|
33
|
+
|
|
34
|
+
| Prop | Event | Prop | Event |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `onCellSelect` | `cell-select` | `onColumnSelect` | `column-select` |
|
|
37
|
+
| `onCellEditStart` | `cell-edit-start` | `onColumnAdd` | `column-add` |
|
|
38
|
+
| `onCellEditCommit` | `cell-edit-commit` | `onColumnDelete` | `column-delete` |
|
|
39
|
+
| `onCellEditCancel` | `cell-edit-cancel` | `onColumnReorder` | `column-reorder` |
|
|
40
|
+
| `onValidationError` | `validation-error` | `onColumnResize` | `column-resize` |
|
|
41
|
+
| `onSortChange` | `sort-change` | `onSelectionChange` | `selection-change` |
|
|
42
|
+
| `onFilterChange` | `filter-change` | `onClipboardCopy` | `clipboard-copy` |
|
|
43
|
+
| `onFilterError` | `filter-error` | `onClipboardCut` | `clipboard-cut` |
|
|
44
|
+
| `onRowAdd` | `row-add` | `onClipboardPaste` | `clipboard-paste` |
|
|
45
|
+
| `onRowDelete` | `row-delete` | `onClipboardError` | `clipboard-error` |
|
|
46
|
+
| `onRowActivate` | `row-activate` | `onUndoStateChange` | `undo-state-change` |
|
|
47
|
+
| `onBatchUpdate` | `batch-update` | `onContextMenu` | `context-menu` |
|
|
48
|
+
| `onRowReorder` | `row-reorder` | `onHeaderContextMenu` | `header-context-menu` |
|
|
49
|
+
| `onColumnVisibilityChange` | `column-visibility-change` | `onCommentChange` | `comment-change` |
|
|
50
|
+
| `onDataImport` | `data-import` | `onFillHandleApply` | `fill-handle-apply` |
|
|
51
|
+
| `onFindReplace` | `find-replace` | | |
|
|
52
|
+
|
|
53
|
+
Every custom event of `<flex-table>` has a prop.
|
|
54
|
+
|
|
55
|
+
Type exports from this entry: `FlexTable`, `FlexTableReactProps`, `ColumnDefinition`, `DataRow`,
|
|
56
|
+
`ColumnType`, `ColumnAlign`, `CellRenderer`, `CellEditor`, `CellValidator`, `ConditionalRule`,
|
|
57
|
+
`SelectionMode`, `DataMode`, `UseODataSourceOptions`, `UseODataSourceResult`,
|
|
58
|
+
`UseArraySourceOptions`, `UseArraySourceResult`.
|
|
59
|
+
|
|
60
|
+
## `useODataSource(url, options)`
|
|
61
|
+
|
|
62
|
+
Fetches paged, sorted, searched rows from an OData v4 endpoint. Bind the result to a
|
|
63
|
+
server-mode table:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useCallback } from 'react';
|
|
67
|
+
import { FlexTableReact, useODataSource } from '@iyulab/flex-table/react';
|
|
68
|
+
|
|
69
|
+
function Orders() {
|
|
70
|
+
const fetcher = useCallback((input: string, init: RequestInit) => fetch(input, init), []);
|
|
71
|
+
const source = useODataSource<Order>('/api/orders', { pageSize: 20, fetcher });
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
|
|
76
|
+
{source.error && <p role="alert">{source.error}</p>}
|
|
77
|
+
<FlexTableReact<Order>
|
|
78
|
+
dataMode="server"
|
|
79
|
+
columns={columns}
|
|
80
|
+
data={source.data}
|
|
81
|
+
loading={source.loading}
|
|
82
|
+
onSortChange={source.onSortChange}
|
|
83
|
+
clearSelectionOnDataChange
|
|
84
|
+
/>
|
|
85
|
+
<button disabled={source.page === 0} onClick={() => source.setPage(source.page - 1)}>Prev</button>
|
|
86
|
+
<span>{source.page + 1} / {Math.max(1, Math.ceil(source.totalCount / 20))}</span>
|
|
87
|
+
<button onClick={() => source.setPage(source.page + 1)}>Next</button>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
| Option | Default | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `pageSize` | `20` | |
|
|
98
|
+
| `defaultOrderBy` | — | `$orderby` string, e.g. `'name asc'` |
|
|
99
|
+
| `initialPage` | `0` | Zero-based; first render only |
|
|
100
|
+
| `initialSearch` | `''` | First render only |
|
|
101
|
+
| `initialSort` | — | `SortCriteria[]`; overrides `defaultOrderBy`; `[]` means no sort |
|
|
102
|
+
| `fixedFilter` | — | odata-query filter object, always applied; value change resets to page 0 (compared by value) |
|
|
103
|
+
| `baseUrl` | `window.location.origin` | For proxy/BFF setups |
|
|
104
|
+
| `fetcher` | global `fetch` | `(input, init) => Promise<Response>`; keep stable |
|
|
105
|
+
| `onUnauthorized` | — | `(response) => void` on 401/403; keep stable |
|
|
106
|
+
| `enabled` | `true` | While `false`: no request, `loading` stays `true`, `refresh()` is a no-op, in-flight request is cancelled |
|
|
107
|
+
|
|
108
|
+
### Result
|
|
109
|
+
|
|
110
|
+
| Field | Notes |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
|
|
113
|
+
| `loading` | Request in flight |
|
|
114
|
+
| `error` | `string \| null` — render it |
|
|
115
|
+
| `page`, `setPage` | Zero-based |
|
|
116
|
+
| `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
|
|
117
|
+
| `search`, `setSearch` | Literal text (resets to page 0) |
|
|
118
|
+
| `refresh` | Re-run the current request |
|
|
119
|
+
|
|
120
|
+
Search is literal: each whitespace-separated token becomes a quoted phrase joined with `AND`
|
|
121
|
+
(`red shirt` → `"red" AND "shirt"`); double quotes are stripped. If the result set shrinks below
|
|
122
|
+
the current page, the page moves down to the last existing page.
|
|
123
|
+
|
|
124
|
+
Use `enabled` when the query depends on an async value:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
const source = useODataSource('/api/orders', {
|
|
128
|
+
fixedFilter: season ? { Season: season } : undefined,
|
|
129
|
+
enabled: season !== undefined,
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## `useArraySource(data, options)`
|
|
134
|
+
|
|
135
|
+
Search, sort and paging over an in-memory array, returning **the same shape** as
|
|
136
|
+
`useODataSource` — the same `dataMode="server"` binding works with either.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useMemo } from 'react';
|
|
140
|
+
import { useArraySource } from '@iyulab/flex-table/react';
|
|
141
|
+
|
|
142
|
+
const joined = useMemo(
|
|
143
|
+
() => prices.map((p) => ({ ...p, productName: productsById[p.productId]?.name ?? '' })),
|
|
144
|
+
[prices, productsById],
|
|
145
|
+
);
|
|
146
|
+
const source = useArraySource(joined, { pageSize: 20, columns });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Option | Default | Notes |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `pageSize` | `20` | |
|
|
152
|
+
| `defaultOrderBy` | — | Same syntax as `useODataSource` |
|
|
153
|
+
| `initialPage` / `initialSearch` / `initialSort` | `0` / `''` / — | Same first-render-only contract |
|
|
154
|
+
| `columns` | — | Enables value-aware sort (number/date/boolean); otherwise text sort |
|
|
155
|
+
| `searchFields` | all row values | `(row) => Array<string \| number \| boolean \| null \| undefined>` |
|
|
156
|
+
|
|
157
|
+
Differences: `totalCount` is the count after search, `loading` is always `false`, `error` always
|
|
158
|
+
`null`, and `refresh` is a no-op. The page is clamped down if the array shrinks.
|
|
159
|
+
|
|
160
|
+
## Without React
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { buildODataQuery, buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
|
|
164
|
+
import { computeArrayView } from '@iyulab/flex-table/array';
|
|
165
|
+
|
|
166
|
+
buildSearchExpression('red shirt'); // '"red" AND "shirt"' ('' → undefined)
|
|
167
|
+
parseOrderBy('name desc'); // [{ key: 'name', direction: 'desc' }]
|
|
168
|
+
buildODataQuery({ page: 2, pageSize: 20, search: 'red', fixedFilter: { IsActive: true } });
|
|
169
|
+
// '?$filter=...&$count=true&$top=20&$skip=40&$search=...'
|
|
170
|
+
|
|
171
|
+
const { data, totalCount } = computeArrayView(rows, {
|
|
172
|
+
search: '', sortCriteria: [], page: 0, pageSize: 20, columns,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`buildODataQuery` state fields: `page`, `pageSize`, and optional `sortCriteria`, `defaultOrderBy`,
|
|
177
|
+
`search`, `fixedFilter` — exactly what the hook sends per request.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Styling, keyboard, localization
|
|
2
|
+
|
|
3
|
+
## Sizing
|
|
4
|
+
|
|
5
|
+
The host is the scroll container and virtualizes against its own height:
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
flex-table { height: 400px; }
|
|
9
|
+
/* or inside a flex column */
|
|
10
|
+
.page { height: 100%; display: flex; flex-direction: column; }
|
|
11
|
+
.page flex-table { flex: 1 1 auto; min-height: 0; }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Theming
|
|
15
|
+
|
|
16
|
+
Every `--ft-*` color falls back to an `@iyulab/components` design token, so loading that token sheet
|
|
17
|
+
makes the grid follow the app theme (e.g. `:root { --u-primary-color: #7b1fa2; }`). Without the sheet,
|
|
18
|
+
literal fallbacks and the built-in dark theme (`prefers-color-scheme` or `theme="dark"`) still apply.
|
|
19
|
+
|
|
20
|
+
Override individual properties to make the table differ from the rest of the app:
|
|
21
|
+
|
|
22
|
+
| Group | Custom properties |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Font | `--ft-font-family`, `--ft-font-size` |
|
|
25
|
+
| Surfaces / text | `--ft-bg`, `--ft-text-color`, `--ft-border-color`, `--ft-editor-bg`, `--ft-empty-color` |
|
|
26
|
+
| Header | `--ft-header-bg`, `--ft-header-hover-bg`, `--ft-header-text-color`, `--ft-sort-indicator-color` |
|
|
27
|
+
| Rows | `--ft-row-even-bg`, `--ft-row-odd-bg`, `--ft-row-hover-bg`, `--ft-row-odd-hover-bg` |
|
|
28
|
+
| Accent | `--ft-active-color`, `--ft-selection-bg`, `--ft-bool-color` |
|
|
29
|
+
| State overlays | `--ft-invalid-color`, `--ft-drop-color`, `--ft-find-color` (translucent backgrounds derive from these) |
|
|
30
|
+
|
|
31
|
+
```css
|
|
32
|
+
flex-table {
|
|
33
|
+
--ft-font-size: 13px;
|
|
34
|
+
--ft-active-color: #1a73e8;
|
|
35
|
+
--ft-row-odd-bg: #fafafa;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Styling custom cell content
|
|
40
|
+
|
|
41
|
+
`render` output lives in the shadow root, so page CSS does not reach it. Pass constructable
|
|
42
|
+
stylesheets instead (reassigning replaces the previous set):
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const sheet = new CSSStyleSheet();
|
|
46
|
+
sheet.replaceSync('.badge { padding: 0 6px; border-radius: 4px; }');
|
|
47
|
+
table.stylesheets = [sheet];
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Density and header hierarchy
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
flex-table {
|
|
54
|
+
--ft-row-height: 28px;
|
|
55
|
+
--ft-cell-padding-block: 4px;
|
|
56
|
+
--ft-cell-padding-inline: 8px;
|
|
57
|
+
--ft-header-font-size: 13px; /* defaults to --ft-font-size */
|
|
58
|
+
--ft-header-font-weight: 600;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `--ft-row-height` is read once at first render and only in `px`; to change it later set the
|
|
63
|
+
`rowHeight` property / `row-height` attribute (which always win over the token).
|
|
64
|
+
- Cells are single-line. When reducing row height, reduce `--ft-cell-padding-block` too
|
|
65
|
+
(`padding-block × 2 + line box ≤ row height`), or text is clipped.
|
|
66
|
+
|
|
67
|
+
## Keyboard
|
|
68
|
+
|
|
69
|
+
| Key | Action |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Arrows / Tab / Shift+Tab | Move between cells |
|
|
72
|
+
| Home / End, Ctrl+Home / Ctrl+End | Row start/end, table start/end |
|
|
73
|
+
| Shift+Arrow, Shift+Click | Extend range selection |
|
|
74
|
+
| Ctrl+Click header | Select column |
|
|
75
|
+
| Enter / F2 | Edit (Enter on a non-editable cell fires `row-activate`) |
|
|
76
|
+
| Typing a printable character | Start editing an editable cell |
|
|
77
|
+
| Escape | Cancel edit / clear selection / close find panel |
|
|
78
|
+
| Delete / Backspace | Clear selected cells |
|
|
79
|
+
| Ctrl+C / Ctrl+X / Ctrl+V | Copy / cut / paste as TSV (Excel / Google Sheets compatible) |
|
|
80
|
+
| Ctrl+D / Ctrl+R | Fill down / fill right |
|
|
81
|
+
| Ctrl+F / Ctrl+H | Find / find and replace |
|
|
82
|
+
| Ctrl+Z, Ctrl+Y or Ctrl+Shift+Z | Undo, redo |
|
|
83
|
+
| Alt+ArrowLeft / Alt+ArrowRight | Resize current column |
|
|
84
|
+
| Enter / Space on a column menu button | Open the column menu (arrows, Home/End navigate; Escape closes) |
|
|
85
|
+
|
|
86
|
+
Ctrl also matches Cmd on macOS. Editing shortcuts are ignored when `editable` is `false`.
|
|
87
|
+
|
|
88
|
+
## Localization
|
|
89
|
+
|
|
90
|
+
The grid's own chrome (column menu, filters, find/replace, context menu, empty states) uses a locale
|
|
91
|
+
namespace. English and Korean are built in and follow the active `@iyulab/components` locale:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Locale } from '@iyulab/components';
|
|
95
|
+
Locale.set('ko');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Add a language or reword strings with partial tables; `FlexTableMessageKey` lists the valid keys:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { flexTableLocale } from '@iyulab/flex-table';
|
|
102
|
+
|
|
103
|
+
flexTableLocale.register('ja', { contains: '含む', startsWith: '前方一致' });
|
|
104
|
+
flexTableLocale.register('en', { replaceAll: 'Replace everything' });
|
|
105
|
+
flexTableLocale.register('ja', { columnMenuFor: '{header} の列メニュー' }); // {header} placeholder
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`emptyMessage` and `noMatchingMessage` can also be set per table.
|