@iyulab/flex-table 0.39.0 → 0.40.1

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,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.
@@ -1,118 +0,0 @@
1
- import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
2
- import i from "odata-query";
3
- //#region src/odata/use-odata-source.ts
4
- function a(e) {
5
- let t = e.split(/\s+/).map((e) => e.replace(/"/g, "")).filter((e) => e.length > 0);
6
- if (t.length !== 0) return t.map((e) => `"${e}"`).join(" AND ");
7
- }
8
- function o(e) {
9
- let { defaultOrderBy: t, initialPage: n = 0, initialSearch: r = "", initialSort: i } = e;
10
- return {
11
- page: n,
12
- search: r,
13
- sortCriteria: i ?? (t ? c(t) : [])
14
- };
15
- }
16
- function s(s, c = {}) {
17
- let { pageSize: l = 20, defaultOrderBy: u, initialPage: d = 0, initialSearch: f = "", initialSort: p, fixedFilter: m, baseUrl: h, fetcher: g = fetch, onUnauthorized: _, enabled: v = !0 } = c, y = m ? JSON.stringify(m) : "", [b, x] = r([]), [S, C] = r(0), [w, T] = r(!1), [E, D] = r(null), O = o({
18
- defaultOrderBy: u,
19
- initialPage: d,
20
- initialSearch: f,
21
- initialSort: p
22
- }), [k, A] = r(O.page), [j, M] = r(O.sortCriteria), [N, P] = r(O.search), [F, I] = r(0), [L, R] = r(y);
23
- y !== L && (R(y), A(0));
24
- let z = n(null), B = e(() => {
25
- I((e) => e + 1);
26
- }, []), V = e((e) => {
27
- P(e), A(0);
28
- }, []), H = e((e) => {
29
- let t = e.detail?.criteria;
30
- t && (M(t), A(0));
31
- }, []);
32
- return t(() => {
33
- if (z.current?.abort(), !v) return;
34
- let e = new AbortController();
35
- z.current = e, T(!0), D(null);
36
- let t = j.length > 0 ? j.map((e) => `${e.key} ${e.direction}`).join(", ") : u, n = {
37
- top: l,
38
- skip: k * l,
39
- count: !0
40
- };
41
- if (t && (n.orderBy = t), m && (n.filter = m), N) {
42
- let e = a(N);
43
- e && (n.search = e);
44
- }
45
- let r = i(n), o = `${h ?? window.location.origin}${s}${r}`, c = async (t) => {
46
- let n = await g(t, { signal: e.signal });
47
- if (!n.ok) {
48
- (n.status === 401 || n.status === 403) && _ && _(n);
49
- let e = await n.text().catch(() => ""), t = `Request failed (${n.status})`;
50
- try {
51
- let n = JSON.parse(e);
52
- t = n?.error?.message ?? n?.message ?? t;
53
- } catch {}
54
- throw Error(t);
55
- }
56
- return n.json();
57
- };
58
- return (async () => {
59
- let e = await c(o), t = [...e.value ?? e], n = new URL(o).origin, r = /* @__PURE__ */ new Set([o]), i = o, a = e["@odata.nextLink"];
60
- for (; typeof a == "string" && a && t.length < l;) {
61
- let e = new URL(a, i);
62
- if (e.origin !== n) throw Error(`OData nextLink points outside the source origin (${e.origin})`);
63
- if (i = e.toString(), r.has(i)) throw Error(`OData nextLink repeats an already-read page (${i})`);
64
- r.add(i);
65
- let o = await c(i);
66
- t.push(...o.value ?? []), a = o["@odata.nextLink"];
67
- }
68
- return {
69
- count: e["@odata.count"],
70
- rows: t
71
- };
72
- })().then(({ count: t, rows: n }) => {
73
- if (e.signal.aborted) return;
74
- let r = t ?? 0;
75
- x(n), C(r), D(null);
76
- let i = r === 0 ? 0 : Math.ceil(r / l) - 1;
77
- k > i && A(i);
78
- }).catch((t) => {
79
- e.signal.aborted || t.name !== "AbortError" && (D(t.message), x([]), C(0));
80
- }).finally(() => {
81
- e.signal.aborted || T(!1);
82
- }), () => e.abort();
83
- }, [
84
- v,
85
- s,
86
- k,
87
- l,
88
- j,
89
- N,
90
- y,
91
- u,
92
- F,
93
- h
94
- ]), {
95
- data: b,
96
- totalCount: S,
97
- loading: w || !v,
98
- error: E,
99
- page: k,
100
- setPage: A,
101
- sortCriteria: j,
102
- onSortChange: H,
103
- search: N,
104
- setSearch: V,
105
- refresh: B
106
- };
107
- }
108
- function c(e) {
109
- return e.split(",").map((e) => {
110
- let t = e.trim().split(/\s+/);
111
- return {
112
- key: t[0],
113
- direction: t[1]?.toLowerCase() === "desc" ? "desc" : "asc"
114
- };
115
- });
116
- }
117
- //#endregion
118
- export { s as i, c as n, o as r, a as t };