@iyulab/flex-table 0.46.0 → 0.48.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/CHANGELOG.md +57 -0
- package/README.md +17 -9
- package/dist/array/types.d.ts +2 -1
- package/dist/core/source-error.d.ts +35 -0
- package/dist/{flex-table-DIQhqj12.js → flex-table-B8ehfxqn.js} +130 -102
- package/dist/flex-table.d.ts +20 -3
- package/dist/flex-table.js +1 -1
- package/dist/odata/types.d.ts +6 -1
- package/dist/react.d.ts +1 -0
- package/dist/react.js +82 -55
- package/package.json +2 -1
- package/skills/iyulab-flex-table/SKILL.md +1 -1
- package/skills/iyulab-flex-table/references/api.md +1 -1
- package/skills/iyulab-flex-table/references/react.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.48.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Ctrl+X no longer clears cells that never reached the clipboard.** The keys called the async
|
|
8
|
+
Clipboard API; when the write was refused — no permission, a page outside a secure context, a
|
|
9
|
+
browser without it — the table still cleared the cut range and fired `clipboard-cut`
|
|
10
|
+
(`clipboard-copy` for Ctrl+C). Ctrl+C, Ctrl+X and Ctrl+V now go through the browser's own
|
|
11
|
+
copy/cut/paste and `clipboardData`: they need no clipboard permission, work on any page, and a cut
|
|
12
|
+
range is cleared only once its text is on the clipboard.
|
|
13
|
+
- The context menu's Copy, which still writes through the Clipboard API, fires `clipboard-copy` only
|
|
14
|
+
when the write succeeds; a refused write fires `clipboard-error` alone.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `clipboard-error` is now only the context menu's Copy (`action: 'copy'`). Paste no longer reads
|
|
19
|
+
through the Clipboard API, so it has no permission to be refused and never reports `'paste'`.
|
|
20
|
+
- Copy, cut and paste typed in a text field inside the grid (the cell editor, the find panel) are
|
|
21
|
+
that field's own and leave the cells alone.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- README: the keyboard table covers every key the grid handles, and the column example uses
|
|
26
|
+
`appearance="plain"` (the `variant` attribute was removed in `@iyulab/components` 2.0).
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
|
|
30
|
+
- Real key input for Ctrl+C/X/V, Ctrl+Click on a header, and the column menu's Space / Home / End /
|
|
31
|
+
Escape, alongside the rest of the keyboard table.
|
|
32
|
+
|
|
33
|
+
## [0.47.0] - 2026-10-05
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Breaking: `error` from `useODataSource` and `useArraySource` is a structured failure, not a
|
|
38
|
+
string** — `SourceError | null`, where `SourceError` is `{ message, status?, code?, details?, body? }`
|
|
39
|
+
(exported from `@iyulab/flex-table/react` with `SourceErrorDetail`). The message alone lost which
|
|
40
|
+
failure it was, so an app could not tell a 403 the server marked with its own code (for example
|
|
41
|
+
"password change required") from any other 403, or a 404 from a 409, without wrapping `fetcher`
|
|
42
|
+
to watch the status. Now `status` is the HTTP status, `code` the server's rejection code (OData
|
|
43
|
+
`error.code`), `details` the OData `error.details` entries, and `body` the parsed response (or its
|
|
44
|
+
text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has
|
|
45
|
+
a `message` only. The message itself is unchanged.
|
|
46
|
+
|
|
47
|
+
Migration: render `error.message` where you rendered `error`
|
|
48
|
+
(`{source.error && <p role="alert">{source.error.message}</p>}`), and compare `error?.message`
|
|
49
|
+
where you compared the string.
|
|
50
|
+
|
|
51
|
+
- **Date and datetime column filters use `u-date-picker`** for their From and To bounds, like the
|
|
52
|
+
cell editor since 0.46. The native date inputs they replace showed the browser's UI language
|
|
53
|
+
(`10/02/2026` in an English browser) while the table shows ISO; the bounds now read and show
|
|
54
|
+
`YYYY-MM-DD` (`YYYY-MM-DD HH:mm`) everywhere, with a calendar beside each. The bounds are labelled
|
|
55
|
+
(**From**, **To**), each calendar stops at the other bound, and either bound may still stay empty.
|
|
56
|
+
A bound now applies when it is committed — Enter, leaving the box, or a day picked in the calendar —
|
|
57
|
+
rather than on every keystroke. While a calendar is open, Escape closes the calendar and the next
|
|
58
|
+
Escape closes the filter. Opening the filter from the column menu focuses the From bound.
|
|
59
|
+
|
|
3
60
|
## [0.46.0] - 2026-10-05
|
|
4
61
|
|
|
5
62
|
### Changed
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ npm install @iyulab/flex-table
|
|
|
50
50
|
- **Range Selection** — Shift+Arrow, Shift+Click for multi-cell selection
|
|
51
51
|
- **Column Selection** — Ctrl+Click header or `selectColumn()` API
|
|
52
52
|
- **Row Selection** — Checkbox-based row selection (`selectable`, single/multi mode)
|
|
53
|
-
- **Clipboard** — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180)
|
|
53
|
+
- **Clipboard** — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180), through the browser's own copy/cut/paste — no clipboard permission needed
|
|
54
54
|
- **Sorting** — Click header to sort (asc/desc/none), Shift+click for multi-sort
|
|
55
55
|
- **Column Menu** — A 24×24 button in every header opens the column's menu: sort, filter, hide/show, auto-fit, wider/narrower (also opens on header right-click)
|
|
56
56
|
- **Column Resize** — Drag header border, double-click to auto-fit, Alt+Arrow keyboard resize, or the column menu
|
|
@@ -208,7 +208,7 @@ hover) always show them, and they are not printed.
|
|
|
208
208
|
|
|
209
209
|
```ts
|
|
210
210
|
{ key: 'actions', label: '', width: 72, editable: false, reveal: 'hover',
|
|
211
|
-
render: (_, row) => html`<u-button size="sm"
|
|
211
|
+
render: (_, row) => html`<u-button size="sm" appearance="plain" aria-label="Edit">…</u-button>` }
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
### Merging Repeated Values
|
|
@@ -334,7 +334,7 @@ All events use `CustomEvent` with `bubbles: true, composed: true`.
|
|
|
334
334
|
| `clipboard-copy` | `{ range, text }` | Range copied as TSV |
|
|
335
335
|
| `clipboard-cut` | `{ range, text }` | Range cut as TSV |
|
|
336
336
|
| `clipboard-paste` | `{ changes, addedRows }` | Data pasted from clipboard |
|
|
337
|
-
| `clipboard-error` | `{ action, error }` |
|
|
337
|
+
| `clipboard-error` | `{ action, error }` | The context menu's Copy could not write to the clipboard (`action`: `'copy'`); no `clipboard-copy` follows |
|
|
338
338
|
|
|
339
339
|
### State Events
|
|
340
340
|
|
|
@@ -429,13 +429,16 @@ package dependency; CSS custom properties are resolved at render time, not impor
|
|
|
429
429
|
|-----|--------|
|
|
430
430
|
| Arrow keys | Navigate cells |
|
|
431
431
|
| Tab / Shift+Tab | Move to next/previous cell |
|
|
432
|
-
| Enter / F2 | Start editing |
|
|
433
|
-
|
|
|
432
|
+
| Enter / F2 | Start editing (Enter on a non-editable cell fires `row-activate`) |
|
|
433
|
+
| Typing a printable character | Start editing an editable cell with that character |
|
|
434
|
+
| Escape | Cancel edit / clear selection / close the find panel |
|
|
434
435
|
| Home / End | Row start/end |
|
|
435
436
|
| Ctrl+Home / Ctrl+End | Table start/end |
|
|
436
|
-
| Shift+Arrow | Extend selection range |
|
|
437
|
+
| Shift+Arrow / Shift+Click | Extend selection range |
|
|
437
438
|
| Ctrl+C / Ctrl+X | Copy/Cut selection as TSV |
|
|
438
439
|
| Ctrl+V | Paste TSV data |
|
|
440
|
+
| Ctrl+D / Ctrl+R | Fill down / fill right |
|
|
441
|
+
| Ctrl+F / Ctrl+H | Find / find and replace |
|
|
439
442
|
| Delete / Backspace | Clear selected cells |
|
|
440
443
|
| Ctrl+Z | Undo |
|
|
441
444
|
| Ctrl+Shift+Z / Ctrl+Y | Redo |
|
|
@@ -684,8 +687,13 @@ button is highlighted while its column has an active filter.
|
|
|
684
687
|
- **text**: case-insensitive substring search
|
|
685
688
|
- **number**: min/max range inputs
|
|
686
689
|
- **boolean**: All / True / False select
|
|
687
|
-
- **date**:
|
|
688
|
-
-
|
|
690
|
+
- **date**: **From** and **To** `u-date-picker`s (`@iyulab/components`) — the cell editor's control, so the
|
|
691
|
+
bounds read and show `YYYY-MM-DD` in every browser language, with a calendar beside each. Either
|
|
692
|
+
bound may stay empty; the end day is inclusive, and each calendar stops at the other bound.
|
|
693
|
+
- **datetime**: the same pickers with a time (`YYYY-MM-DD HH:mm`)
|
|
694
|
+
|
|
695
|
+
A bound applies when it is committed — Enter, leaving the box, or a day picked in its calendar. While a
|
|
696
|
+
calendar is open, Escape closes the calendar; the next Escape closes the filter.
|
|
689
697
|
|
|
690
698
|
Filters set via the UI and the programmatic API (`setFilter()`) share the same filter state. Filter dropdowns automatically flip upward when near the viewport bottom.
|
|
691
699
|
|
|
@@ -787,7 +795,7 @@ The hook returns:
|
|
|
787
795
|
|---|---|
|
|
788
796
|
| `data` / `totalCount` | Current page rows and the server's total (`@odata.count`). When the server pages its response (`@odata.nextLink`, e.g. a page size smaller than `pageSize`), the hook follows the link until the page is filled; a link outside the request's origin, or one that returns to a page already read, is reported through `error` instead of showing a short page |
|
|
789
797
|
| `loading` | A request is in flight |
|
|
790
|
-
| `error` |
|
|
798
|
+
| `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **Render `error.message`** — a failed request otherwise leaves the grid silently empty. Branch on `status` (HTTP), `code` (the server's rejection code, OData `error.code`) and `details` (OData `error.details`); `body` is the parsed response (or its text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has a `message` only |
|
|
791
799
|
| `page` / `setPage` | Zero-based page index |
|
|
792
800
|
| `sortCriteria` / `onSortChange` | Bind `onSortChange` to the table's `sort-change` event |
|
|
793
801
|
| `search` / `setSearch` | Current search term and its setter (resets to page 0) |
|
package/dist/array/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SortCriteria } from '../core/sorting.js';
|
|
2
|
+
import type { SourceError } from '../core/source-error.js';
|
|
2
3
|
export interface UseArraySourceOptions<T> {
|
|
3
4
|
/** 페이지당 행 수. `useODataSource`와 동일 기본값. */
|
|
4
5
|
pageSize?: number;
|
|
@@ -36,7 +37,7 @@ export interface UseArraySourceResult<T> {
|
|
|
36
37
|
/** 항상 `false` — 로컬 배열은 동기 처리라 로딩 상태가 없다. `useODataSource`와의 반환 형태 동일성을 위해 유지. */
|
|
37
38
|
loading: boolean;
|
|
38
39
|
/** 항상 `null` — 로컬 배열 처리는 실패하지 않는다. 위와 같은 이유로 유지. */
|
|
39
|
-
error:
|
|
40
|
+
error: SourceError | null;
|
|
40
41
|
page: number;
|
|
41
42
|
setPage: (page: number) => void;
|
|
42
43
|
sortCriteria: SortCriteria[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** OData v4 오류 봉투의 `error.details` 항목 — 필드별 검증 실패 상세. */
|
|
2
|
+
export interface SourceErrorDetail {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
target?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 데이터 소스 훅(`useODataSource`/`useArraySource`)의 실패 값.
|
|
9
|
+
*
|
|
10
|
+
* 문자열 하나가 아니라 구조로 싣는 이유: 소비자는 «어떤 실패인가» 로 갈라야 한다 — 403 중에서도
|
|
11
|
+
* 서버가 코드로 구분한 거절(«비밀번호를 바꿔야 한다»), 404(지워진 행), 409(동시성), 429(제한).
|
|
12
|
+
* 메시지만 남기면 그 정보가 소스 경계에서 사라지고, 소비자는 전송(`fetcher`)을 감싸 상태를
|
|
13
|
+
* 엿보는 것 말고는 길이 없다.
|
|
14
|
+
*/
|
|
15
|
+
export interface SourceError {
|
|
16
|
+
/** 보여 줄 문장 — 서버가 준 메시지가 있으면 그것, 없으면 `Request failed (<status>)`. */
|
|
17
|
+
message: string;
|
|
18
|
+
/** HTTP 상태. 응답이 없던 실패(네트워크 오류 · `@odata.nextLink` 검사)에는 없다. */
|
|
19
|
+
status?: number;
|
|
20
|
+
/** 서버가 정한 거절 코드 — OData 오류 봉투의 `error.code`(또는 최상위 `code`). */
|
|
21
|
+
code?: string;
|
|
22
|
+
/** 오류 봉투의 `error.details` 중 `code`·`message` 가 둘 다 문자열인 항목. */
|
|
23
|
+
details?: SourceErrorDetail[];
|
|
24
|
+
/** 응답 본문 — JSON 이면 파싱한 값, 아니면 텍스트. 비어 있으면 없다. */
|
|
25
|
+
body?: unknown;
|
|
26
|
+
}
|
|
27
|
+
/** 실패 응답을 `SourceError` 로 읽는다. 본문을 읽을 수 없어도 상태와 기본 문장은 남긴다. */
|
|
28
|
+
export declare function readFailedResponse(res: Response): Promise<SourceError>;
|
|
29
|
+
/** 실패 응답을 실어 나르는 내부 예외 — `catch` 가 상태를 잃지 않고 `SourceError` 를 꺼낸다. */
|
|
30
|
+
export declare class SourceRequestError extends Error {
|
|
31
|
+
readonly failure: SourceError;
|
|
32
|
+
constructor(failure: SourceError);
|
|
33
|
+
}
|
|
34
|
+
/** 잡힌 예외를 `SourceError` 로 — 응답이 있던 실패는 그 구조 그대로, 나머지는 메시지만. */
|
|
35
|
+
export declare function toSourceError(err: unknown): SourceError;
|
|
@@ -567,6 +567,19 @@ var v = r`
|
|
|
567
567
|
width: 50%;
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
/* Date bounds stack: side by side, half of the dropdown is narrower than a date (and far narrower
|
|
571
|
+
than a date and a time) plus the calendar button. */
|
|
572
|
+
.ft-filter-range-dates {
|
|
573
|
+
flex-direction: column;
|
|
574
|
+
gap: 6px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.ft-filter-date {
|
|
578
|
+
display: block;
|
|
579
|
+
width: 100%;
|
|
580
|
+
min-width: 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
570
583
|
.ft-filter-actions {
|
|
571
584
|
margin-top: 6px;
|
|
572
585
|
text-align: right;
|
|
@@ -1842,16 +1855,20 @@ function K(e, t, n, r) {
|
|
|
1842
1855
|
}
|
|
1843
1856
|
//#endregion
|
|
1844
1857
|
//#region src/flex-table.ts
|
|
1845
|
-
var q =
|
|
1858
|
+
var q = [
|
|
1859
|
+
"copy",
|
|
1860
|
+
"cut",
|
|
1861
|
+
"paste"
|
|
1862
|
+
], J = {
|
|
1846
1863
|
eq: "=",
|
|
1847
1864
|
neq: "≠",
|
|
1848
1865
|
gt: ">",
|
|
1849
1866
|
lt: "<",
|
|
1850
1867
|
gte: "≥",
|
|
1851
1868
|
lte: "≤"
|
|
1852
|
-
},
|
|
1869
|
+
}, Y = 120, X = 40, qe = 20, Je = 32, Z = 5, Ye = 200, Q = class extends n {
|
|
1853
1870
|
constructor(...e) {
|
|
1854
|
-
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss =
|
|
1871
|
+
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Je, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new ne(), this._editing = new re(), this._rowSelection = new j(), this._undo = new M(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onCommentPopupOutsideClick = () => {
|
|
1855
1872
|
this._commitCommentPopup();
|
|
1856
1873
|
}, this._invalidCells = /* @__PURE__ */ new Map(), this._textFilterState = /* @__PURE__ */ new Map(), this._numberFilterState = /* @__PURE__ */ new Map(), this._dateFilterState = /* @__PURE__ */ new Map(), this._emptyFilterState = /* @__PURE__ */ new Map();
|
|
1857
1874
|
}
|
|
@@ -1866,7 +1883,7 @@ var q = {
|
|
|
1866
1883
|
this._data = e, n && !this._inUndoRedo && (this.clearUndoOnDataChange && this._undo.clear(), this.clearSelectionOnDataChange && this._rowSelection.selectedCount > 0 && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent())), this.requestUpdate("data", t);
|
|
1867
1884
|
}
|
|
1868
1885
|
get rowHeight() {
|
|
1869
|
-
let e = this._rowHeightExplicit ?? this._rowHeightFromCss ??
|
|
1886
|
+
let e = this._rowHeightExplicit ?? this._rowHeightFromCss ?? Je;
|
|
1870
1887
|
return Math.max(e, this._targetFloor);
|
|
1871
1888
|
}
|
|
1872
1889
|
set rowHeight(e) {
|
|
@@ -2391,8 +2408,8 @@ var q = {
|
|
|
2391
2408
|
return this._columnWidths.get(e);
|
|
2392
2409
|
}
|
|
2393
2410
|
_getColWidth(e) {
|
|
2394
|
-
let t = this._columnWidths.get(e.key) ?? e.width ??
|
|
2395
|
-
return Math.max(t, e.minWidth ??
|
|
2411
|
+
let t = this._columnWidths.get(e.key) ?? e.width ?? Y;
|
|
2412
|
+
return Math.max(t, e.minWidth ?? X);
|
|
2396
2413
|
}
|
|
2397
2414
|
get headerHeight() {
|
|
2398
2415
|
return this.rowHeight + 8;
|
|
@@ -2418,17 +2435,21 @@ var q = {
|
|
|
2418
2435
|
return Math.max(0, this._visibleRowCount - this._frozenRowCount) * this.rowHeight;
|
|
2419
2436
|
}
|
|
2420
2437
|
get visibleRange() {
|
|
2421
|
-
let e = this._frozenRowCount, t = e + Math.floor(Math.max(0, this._scrollTop) / this.rowHeight), n = Math.max(e, t -
|
|
2438
|
+
let e = this._frozenRowCount, t = e + Math.floor(Math.max(0, this._scrollTop) / this.rowHeight), n = Math.max(e, t - Z), r = Math.max(0, this._viewportHeight - this.headerHeight - this.frozenRowsHeight), i = Math.ceil(r / this.rowHeight) + 1;
|
|
2422
2439
|
return {
|
|
2423
2440
|
start: n,
|
|
2424
|
-
end: Math.min(this._visibleRowCount, t + i +
|
|
2441
|
+
end: Math.min(this._visibleRowCount, t + i + Z)
|
|
2425
2442
|
};
|
|
2426
2443
|
}
|
|
2427
2444
|
connectedCallback() {
|
|
2428
|
-
super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this.
|
|
2445
|
+
super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this._onClipboard = this._onClipboard.bind(this), this.addEventListener("scroll", this._onScroll, { passive: !0 }), this.addEventListener("keydown", this._onKeyDown);
|
|
2446
|
+
for (let e of q) this.addEventListener(e, this._onClipboard);
|
|
2447
|
+
this.addEventListener("contextmenu", this._onContextMenu), this.addEventListener("dragover", this._onDragover), this.addEventListener("dragleave", this._onDragleave), this.addEventListener("drop", this._onDrop), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "grid"), typeof ResizeObserver < "u" && (this._hostResizeObserver = new ResizeObserver(() => this._measureViewport()), this._hostResizeObserver.observe(this));
|
|
2429
2448
|
}
|
|
2430
2449
|
disconnectedCallback() {
|
|
2431
|
-
super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown)
|
|
2450
|
+
super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown);
|
|
2451
|
+
for (let e of q) this.removeEventListener(e, this._onClipboard);
|
|
2452
|
+
this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
|
|
2432
2453
|
}
|
|
2433
2454
|
firstUpdated() {
|
|
2434
2455
|
this._readDensityTokens(), this._measureViewport(), this.shadowRoot && Array.isArray(this.shadowRoot.adoptedStyleSheets) && (this._baseStyleSheets = [...this.shadowRoot.adoptedStyleSheets]), this._syncStylesheets();
|
|
@@ -2464,13 +2485,13 @@ var q = {
|
|
|
2464
2485
|
i = r;
|
|
2465
2486
|
break;
|
|
2466
2487
|
}
|
|
2467
|
-
i = Math.max(0, i -
|
|
2488
|
+
i = Math.max(0, i - Z);
|
|
2468
2489
|
let a = e.length;
|
|
2469
2490
|
for (let e = i; e < t.length; e++) if (t[e] > r) {
|
|
2470
2491
|
a = e;
|
|
2471
2492
|
break;
|
|
2472
2493
|
}
|
|
2473
|
-
return a = Math.min(e.length, a +
|
|
2494
|
+
return a = Math.min(e.length, a + Z), {
|
|
2474
2495
|
start: i,
|
|
2475
2496
|
end: a
|
|
2476
2497
|
};
|
|
@@ -2530,7 +2551,7 @@ var q = {
|
|
|
2530
2551
|
e !== this._viewportHeight && (this._viewportHeight = e), t !== this._viewportWidth && (this._viewportWidth = t), this._warnIfUnconstrained();
|
|
2531
2552
|
}
|
|
2532
2553
|
_warnIfUnconstrained() {
|
|
2533
|
-
this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount <
|
|
2554
|
+
this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount < Ye || this.scrollHeight > this.clientHeight + 1 || (this._warnedUnconstrained = !0, console.warn(`[@iyulab/flex-table] ${this._visibleRowCount} rows but the host has no height constraint — it grew to fit every row, so virtual scrolling is off and all rows are rendered. Give the host a height (e.g. height: 400px, or flex: 1 1 auto with min-height: 0 inside a sized parent) so it becomes the scroll container.`));
|
|
2534
2555
|
}
|
|
2535
2556
|
_onScroll() {
|
|
2536
2557
|
this._scrollTop = this.scrollTop, this._scrollLeft = this.scrollLeft;
|
|
@@ -2766,7 +2787,7 @@ var q = {
|
|
|
2766
2787
|
_onEditorKeyDown(e) {
|
|
2767
2788
|
if (_(e)) return;
|
|
2768
2789
|
let t = e.currentTarget;
|
|
2769
|
-
if (!(t?.localName === "u-date-picker" && (e.composedPath().some((e) => e instanceof Element && e.part?.contains("popover")) || e.key === "Escape" &&
|
|
2790
|
+
if (!(t?.localName === "u-date-picker" && (e.composedPath().some((e) => e instanceof Element && e.part?.contains("popover")) || e.key === "Escape" && $(t)))) {
|
|
2770
2791
|
if (this._autocompleteState && this._autocompleteState.candidates.length > 0) {
|
|
2771
2792
|
if (e.key === "ArrowDown") {
|
|
2772
2793
|
e.preventDefault(), e.stopPropagation();
|
|
@@ -2822,7 +2843,7 @@ var q = {
|
|
|
2822
2843
|
this._activeCell = this._selection.activeCell ? { ...this._selection.activeCell } : null, this._scrollToActiveCell(), this._dispatchSelectionEvent();
|
|
2823
2844
|
}
|
|
2824
2845
|
_onKeyDown(e) {
|
|
2825
|
-
if (this._editing.current) return;
|
|
2846
|
+
if (this._editing.current || this._openFilterKey && e.composedPath().some((e) => e instanceof Element && e.classList.contains("ft-filter-dropdown"))) return;
|
|
2826
2847
|
let t = this.visibleColumns;
|
|
2827
2848
|
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
|
|
2828
2849
|
let t = e.key.toLowerCase();
|
|
@@ -2874,9 +2895,9 @@ var q = {
|
|
|
2874
2895
|
switch (e.key.toLowerCase()) {
|
|
2875
2896
|
case "z": return e.preventDefault(), e.shiftKey ? this.redo() : this.undo(), !0;
|
|
2876
2897
|
case "y": return e.preventDefault(), this.redo(), !0;
|
|
2877
|
-
case "c":
|
|
2878
|
-
case "x":
|
|
2879
|
-
case "v": return
|
|
2898
|
+
case "c":
|
|
2899
|
+
case "x":
|
|
2900
|
+
case "v": return !0;
|
|
2880
2901
|
case "d": return e.preventDefault(), this.editable && this._handleFillDown(), !0;
|
|
2881
2902
|
case "r": return e.preventDefault(), this.editable && this._handleFillRight(), !0;
|
|
2882
2903
|
case "f": return e.preventDefault(), this._toggleFindPanel("find"), !0;
|
|
@@ -2888,12 +2909,12 @@ var q = {
|
|
|
2888
2909
|
if (!e.altKey || !this._activeCell || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return !1;
|
|
2889
2910
|
e.preventDefault();
|
|
2890
2911
|
let n = t[this._activeCell.col];
|
|
2891
|
-
return n && this._resizeColumnBy(n.key, e.key === "ArrowRight" ?
|
|
2912
|
+
return n && this._resizeColumnBy(n.key, e.key === "ArrowRight" ? qe : -20), !0;
|
|
2892
2913
|
}
|
|
2893
2914
|
_resizeColumnBy(e, t) {
|
|
2894
2915
|
let n = this.visibleColumns.findIndex((t) => t.key === e), r = this.visibleColumns[n];
|
|
2895
2916
|
if (!r) return;
|
|
2896
|
-
let i = this._columnWidths.get(r.key) ?? r.width ??
|
|
2917
|
+
let i = this._columnWidths.get(r.key) ?? r.width ?? Y, a = r.minWidth ?? X, o = Math.max(a, i + t);
|
|
2897
2918
|
this._columnWidths.set(r.key, o), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
|
|
2898
2919
|
detail: {
|
|
2899
2920
|
key: r.key,
|
|
@@ -2919,35 +2940,55 @@ var q = {
|
|
|
2919
2940
|
default: return !1;
|
|
2920
2941
|
}
|
|
2921
2942
|
}
|
|
2922
|
-
|
|
2923
|
-
let
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
}));
|
|
2943
|
+
_selectionTsv() {
|
|
2944
|
+
let e = this._selection.getEffectiveRange();
|
|
2945
|
+
return e ? {
|
|
2946
|
+
range: e,
|
|
2947
|
+
text: oe(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e)
|
|
2948
|
+
} : null;
|
|
2949
|
+
}
|
|
2950
|
+
_onClipboard(e) {
|
|
2951
|
+
let t = e.composedPath()[0];
|
|
2952
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLElement && t.isContentEditable || !e.clipboardData) return;
|
|
2953
|
+
if (e.type === "paste") {
|
|
2954
|
+
if (!this.editable || !this._activeCell) return;
|
|
2955
|
+
e.preventDefault(), this._pasteText(e.clipboardData.getData("text/plain"));
|
|
2956
|
+
return;
|
|
2937
2957
|
}
|
|
2938
|
-
|
|
2958
|
+
let n = this._selectionTsv();
|
|
2959
|
+
n && (e.preventDefault(), e.clipboardData.setData("text/plain", n.text), this._afterCopy(n.range, n.text, e.type === "cut" && this.editable));
|
|
2960
|
+
}
|
|
2961
|
+
async _copyFromMenu() {
|
|
2962
|
+
let e = this._selectionTsv();
|
|
2963
|
+
if (e) {
|
|
2964
|
+
try {
|
|
2965
|
+
await navigator.clipboard.writeText(e.text);
|
|
2966
|
+
} catch (e) {
|
|
2967
|
+
this.dispatchEvent(new CustomEvent("clipboard-error", {
|
|
2968
|
+
detail: {
|
|
2969
|
+
action: "copy",
|
|
2970
|
+
error: e
|
|
2971
|
+
},
|
|
2972
|
+
bubbles: !0,
|
|
2973
|
+
composed: !0
|
|
2974
|
+
}));
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
this._afterCopy(e.range, e.text, !1);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
_afterCopy(e, t, n) {
|
|
2981
|
+
n && this._clearRange(e), this.dispatchEvent(new CustomEvent(n ? "clipboard-cut" : "clipboard-copy", {
|
|
2939
2982
|
detail: {
|
|
2940
|
-
range:
|
|
2941
|
-
text:
|
|
2983
|
+
range: e,
|
|
2984
|
+
text: t
|
|
2942
2985
|
},
|
|
2943
2986
|
bubbles: !0,
|
|
2944
2987
|
composed: !0
|
|
2945
2988
|
}));
|
|
2946
2989
|
}
|
|
2947
|
-
|
|
2990
|
+
_pasteText(e) {
|
|
2948
2991
|
if (!this._activeCell) return;
|
|
2949
|
-
let e = await this._readClipboardText();
|
|
2950
|
-
if (e == null) return;
|
|
2951
2992
|
let t = h(e);
|
|
2952
2993
|
if (t.length === 0) return;
|
|
2953
2994
|
let n = this._expandRowsForPaste(this._activeCell.row, t.length), r = this._applyPasteData(this._activeCell, t);
|
|
@@ -2975,20 +3016,6 @@ var q = {
|
|
|
2975
3016
|
composed: !0
|
|
2976
3017
|
}));
|
|
2977
3018
|
}
|
|
2978
|
-
async _readClipboardText() {
|
|
2979
|
-
try {
|
|
2980
|
-
return await navigator.clipboard.readText();
|
|
2981
|
-
} catch (e) {
|
|
2982
|
-
return this.dispatchEvent(new CustomEvent("clipboard-error", {
|
|
2983
|
-
detail: {
|
|
2984
|
-
action: "paste",
|
|
2985
|
-
error: e
|
|
2986
|
-
},
|
|
2987
|
-
bubbles: !0,
|
|
2988
|
-
composed: !0
|
|
2989
|
-
})), null;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
3019
|
_selectPastedRange(e, t) {
|
|
2993
3020
|
let n = Math.max(...t.map((e) => e.length)), r = Math.min(e.row + t.length, this._visibleRowCount) - 1, i = Math.min(e.col + n, this.visibleColumns.length) - 1;
|
|
2994
3021
|
(r !== e.row || i !== e.col) && (this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length), this._selection.setActive(e.row, e.col), this._selection.setActiveWithRange(r, i), this._activeCell = {
|
|
@@ -3349,8 +3376,9 @@ var q = {
|
|
|
3349
3376
|
_openFilterFromMenu(e) {
|
|
3350
3377
|
this._headerMenu = null, this._openFilterKey = e, requestAnimationFrame(() => {
|
|
3351
3378
|
document.addEventListener("click", this._onDocumentClick, { once: !0 });
|
|
3352
|
-
}), this.updateComplete.then(() => {
|
|
3353
|
-
this.shadowRoot?.querySelector(".ft-filter-dropdown input, .ft-filter-dropdown select")
|
|
3379
|
+
}), this.updateComplete.then(async () => {
|
|
3380
|
+
let e = this.shadowRoot?.querySelector(".ft-filter-dropdown input, .ft-filter-dropdown select, .ft-filter-dropdown u-date-picker");
|
|
3381
|
+
await e?.updateComplete, e?.focus();
|
|
3354
3382
|
});
|
|
3355
3383
|
}
|
|
3356
3384
|
_showHiddenBefore(e) {
|
|
@@ -3386,7 +3414,7 @@ var q = {
|
|
|
3386
3414
|
${r.map((e) => c("show", A("showColumn", { header: e.label }), () => this._setColumnHidden(e.key, !1)))}
|
|
3387
3415
|
<div class="ft-header-menu-separator" role="separator"></div>
|
|
3388
3416
|
${c("autofit", A("autoFitWidth"), () => this._autoFitColumn(e))}
|
|
3389
|
-
${c("wider", A("wider"), () => this._resizeColumnBy(e,
|
|
3417
|
+
${c("wider", A("wider"), () => this._resizeColumnBy(e, qe), !0)}
|
|
3390
3418
|
${c("narrower", A("narrower"), () => this._resizeColumnBy(e, -20), !0)}
|
|
3391
3419
|
</div>
|
|
3392
3420
|
`;
|
|
@@ -3409,7 +3437,7 @@ var q = {
|
|
|
3409
3437
|
style="position: fixed; left: ${n}px; top: ${r}px; z-index: var(--u-layer-floating, 1000);"
|
|
3410
3438
|
@mousedown=${(e) => e.stopPropagation()}
|
|
3411
3439
|
@keydown=${(e) => this._onMenuKeydown(e, ".ft-body-context-menu", l)}>
|
|
3412
|
-
${d(A("copy"), () => this.
|
|
3440
|
+
${d(A("copy"), () => this._copyFromMenu())}
|
|
3413
3441
|
${f}
|
|
3414
3442
|
${d(A("insertRowAbove"), () => this.addRow(void 0, t))}
|
|
3415
3443
|
${d(A("insertRowBelow"), () => this.addRow(void 0, t + 1))}
|
|
@@ -3675,7 +3703,7 @@ var q = {
|
|
|
3675
3703
|
@keydown=${(e) => {
|
|
3676
3704
|
e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
|
|
3677
3705
|
}}>
|
|
3678
|
-
${Object.keys(
|
|
3706
|
+
${Object.keys(J).map((e) => i`<option value=${e}>${J[e]}</option>`)}
|
|
3679
3707
|
</select>
|
|
3680
3708
|
<input class="ft-filter-input ft-num-cond-input" type="text" inputmode="decimal" placeholder=${A("valuePlaceholder")}
|
|
3681
3709
|
.value=${n.text ?? (n.value == null ? "" : P(n.value))}
|
|
@@ -3737,21 +3765,21 @@ var q = {
|
|
|
3737
3765
|
return n ? n.error : null;
|
|
3738
3766
|
}
|
|
3739
3767
|
_renderDateFilter(e) {
|
|
3740
|
-
let t = e.type === "datetime"
|
|
3768
|
+
let t = e.type === "datetime", n = this._dateFilterState.get(e.key) ?? {}, r = (e) => e?.slice(0, 10), o = (e) => {
|
|
3769
|
+
e.key === "Escape" && $(e.currentTarget) || (e.stopPropagation(), e.key === "Escape" && (this._openFilterKey = null));
|
|
3770
|
+
};
|
|
3741
3771
|
return i`
|
|
3742
|
-
<div class="ft-filter-range">
|
|
3743
|
-
<
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
@
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
.value=${n.to ?? ""}
|
|
3751
|
-
@
|
|
3752
|
-
@keydown=${
|
|
3753
|
-
e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
|
|
3754
|
-
}}>
|
|
3772
|
+
<div class="ft-filter-range ft-filter-range-dates">
|
|
3773
|
+
<u-date-picker class="ft-filter-date" size="sm" clearable
|
|
3774
|
+
mode=${t ? "datetime" : "date"} label=${A("fromPlaceholder")}
|
|
3775
|
+
.value=${n.from ?? ""} max=${r(n.to) ?? a}
|
|
3776
|
+
@change=${(t) => this._applyDateFilter(e.key, t, "from")}
|
|
3777
|
+
@keydown=${o}></u-date-picker>
|
|
3778
|
+
<u-date-picker class="ft-filter-date" size="sm" clearable
|
|
3779
|
+
mode=${t ? "datetime" : "date"} label=${A("toPlaceholder")}
|
|
3780
|
+
.value=${n.to ?? ""} min=${r(n.from) ?? a}
|
|
3781
|
+
@change=${(t) => this._applyDateFilter(e.key, t, "to")}
|
|
3782
|
+
@keydown=${o}></u-date-picker>
|
|
3755
3783
|
</div>
|
|
3756
3784
|
${this._renderEmptyFilterRow(e.key, () => {
|
|
3757
3785
|
this._dateFilterState.delete(e.key);
|
|
@@ -3759,7 +3787,7 @@ var q = {
|
|
|
3759
3787
|
`;
|
|
3760
3788
|
}
|
|
3761
3789
|
_applyDateFilter(e, t, n) {
|
|
3762
|
-
let r = t.target.value, i = this._dateFilterState.get(e) ?? {};
|
|
3790
|
+
let r = t.target.value ?? "", i = this._dateFilterState.get(e) ?? {};
|
|
3763
3791
|
r === "" ? delete i[n] : i[n] = r, this._dateFilterState.set(e, i), this._emptyFilterState.delete(e), this._applyFilterForKey(e);
|
|
3764
3792
|
}
|
|
3765
3793
|
_renderBooleanFilter(e) {
|
|
@@ -3787,7 +3815,7 @@ var q = {
|
|
|
3787
3815
|
if (!n) return;
|
|
3788
3816
|
let r = this.shadowRoot?.querySelectorAll(`.ft-cell[data-col-index="${t}"]`), i = 0, a = this.shadowRoot?.querySelector(`.ft-header-cell[data-col-index="${t}"]`);
|
|
3789
3817
|
if (a && (i = Math.max(i, a.scrollWidth)), r) for (let e of r) i = Math.max(i, e.scrollWidth);
|
|
3790
|
-
let o = n.minWidth ??
|
|
3818
|
+
let o = n.minWidth ?? X, s = Math.max(o, i + 8);
|
|
3791
3819
|
this._columnWidths.set(n.key, s), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
|
|
3792
3820
|
detail: {
|
|
3793
3821
|
key: n.key,
|
|
@@ -3800,7 +3828,7 @@ var q = {
|
|
|
3800
3828
|
}
|
|
3801
3829
|
_onResizeStart(e, t) {
|
|
3802
3830
|
e.preventDefault(), e.stopPropagation();
|
|
3803
|
-
let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ??
|
|
3831
|
+
let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? Y;
|
|
3804
3832
|
this._resizing = {
|
|
3805
3833
|
colIndex: t,
|
|
3806
3834
|
startX: e.clientX,
|
|
@@ -3808,13 +3836,13 @@ var q = {
|
|
|
3808
3836
|
};
|
|
3809
3837
|
let i = (e) => {
|
|
3810
3838
|
if (!this._resizing) return;
|
|
3811
|
-
let t = e.clientX - this._resizing.startX, r = n.minWidth ??
|
|
3839
|
+
let t = e.clientX - this._resizing.startX, r = n.minWidth ?? X, i = Math.max(r, this._resizing.startWidth + t);
|
|
3812
3840
|
this._columnWidths.set(n.key, i), this.requestUpdate();
|
|
3813
3841
|
}, a = () => {
|
|
3814
3842
|
document.removeEventListener("mousemove", i), document.removeEventListener("mouseup", o), this._resizeCleanup = null;
|
|
3815
3843
|
}, o = () => {
|
|
3816
3844
|
if (a(), this._resizing) {
|
|
3817
|
-
let e = this._columnWidths.get(n.key) ??
|
|
3845
|
+
let e = this._columnWidths.get(n.key) ?? Y;
|
|
3818
3846
|
this.dispatchEvent(new CustomEvent("column-resize", {
|
|
3819
3847
|
detail: {
|
|
3820
3848
|
key: n.key,
|
|
@@ -4450,7 +4478,7 @@ var q = {
|
|
|
4450
4478
|
return i`
|
|
4451
4479
|
<u-date-picker class="ft-editor ft-editor-${t.type}" size="sm"
|
|
4452
4480
|
mode=${e ? "datetime" : "date"} ?confirm=${e}
|
|
4453
|
-
.value=${
|
|
4481
|
+
.value=${Xe(n, e)}
|
|
4454
4482
|
@keydown=${this._onEditorKeyDown}
|
|
4455
4483
|
@change=${() => this._commitEdit()}
|
|
4456
4484
|
@blur=${() => this._commitEdit()}></u-date-picker>
|
|
@@ -4497,62 +4525,62 @@ var q = {
|
|
|
4497
4525
|
`;
|
|
4498
4526
|
}
|
|
4499
4527
|
};
|
|
4500
|
-
K([s({ type: Array })],
|
|
4528
|
+
K([s({ type: Array })], Q.prototype, "columns", void 0), K([s({
|
|
4501
4529
|
type: Array,
|
|
4502
4530
|
hasChanged: () => !0
|
|
4503
|
-
})],
|
|
4531
|
+
})], Q.prototype, "data", null), K([s({
|
|
4504
4532
|
type: Boolean,
|
|
4505
4533
|
attribute: "clear-undo-on-data-change"
|
|
4506
|
-
})],
|
|
4534
|
+
})], Q.prototype, "clearUndoOnDataChange", void 0), K([s({
|
|
4507
4535
|
type: Boolean,
|
|
4508
4536
|
attribute: "clear-selection-on-data-change"
|
|
4509
|
-
})],
|
|
4537
|
+
})], Q.prototype, "clearSelectionOnDataChange", void 0), K([s({
|
|
4510
4538
|
type: Number,
|
|
4511
4539
|
attribute: "row-height"
|
|
4512
|
-
})],
|
|
4540
|
+
})], Q.prototype, "rowHeight", null), K([s({
|
|
4513
4541
|
type: Boolean,
|
|
4514
4542
|
attribute: "show-row-numbers"
|
|
4515
|
-
})],
|
|
4543
|
+
})], Q.prototype, "showRowNumbers", void 0), K([s({
|
|
4516
4544
|
type: String,
|
|
4517
4545
|
reflect: !0
|
|
4518
|
-
})],
|
|
4546
|
+
})], Q.prototype, "theme", void 0), K([s({
|
|
4519
4547
|
type: Number,
|
|
4520
4548
|
attribute: "max-rows"
|
|
4521
|
-
})],
|
|
4549
|
+
})], Q.prototype, "maxRows", void 0), K([s({ type: Boolean })], Q.prototype, "editable", void 0), K([s({
|
|
4522
4550
|
type: Boolean,
|
|
4523
4551
|
attribute: "show-filters"
|
|
4524
|
-
})],
|
|
4552
|
+
})], Q.prototype, "showFilters", void 0), K([s({
|
|
4525
4553
|
type: Boolean,
|
|
4526
4554
|
attribute: "show-context-menu"
|
|
4527
|
-
})],
|
|
4555
|
+
})], Q.prototype, "showContextMenu", void 0), K([s({
|
|
4528
4556
|
type: Number,
|
|
4529
4557
|
attribute: "frozen-rows"
|
|
4530
|
-
})],
|
|
4558
|
+
})], Q.prototype, "frozenRows", void 0), K([s({
|
|
4531
4559
|
type: String,
|
|
4532
4560
|
attribute: "empty-message"
|
|
4533
|
-
})],
|
|
4561
|
+
})], Q.prototype, "emptyMessage", void 0), K([s({
|
|
4534
4562
|
type: String,
|
|
4535
4563
|
attribute: "no-matching-message"
|
|
4536
|
-
})],
|
|
4564
|
+
})], Q.prototype, "noMatchingMessage", void 0), K([s({
|
|
4537
4565
|
type: Boolean,
|
|
4538
4566
|
reflect: !0
|
|
4539
|
-
})],
|
|
4567
|
+
})], Q.prototype, "loading", void 0), K([s({
|
|
4540
4568
|
type: Boolean,
|
|
4541
4569
|
attribute: "import-enabled"
|
|
4542
|
-
})],
|
|
4570
|
+
})], Q.prototype, "importEnabled", void 0), K([s({ type: Boolean })], Q.prototype, "selectable", void 0), K([s({
|
|
4543
4571
|
type: String,
|
|
4544
4572
|
attribute: "selection-mode"
|
|
4545
|
-
})],
|
|
4573
|
+
})], Q.prototype, "selectionMode", null), K([s({
|
|
4546
4574
|
type: String,
|
|
4547
4575
|
attribute: "data-mode"
|
|
4548
|
-
})],
|
|
4576
|
+
})], Q.prototype, "dataMode", void 0), K([s({
|
|
4549
4577
|
type: Object,
|
|
4550
4578
|
attribute: "footer-data"
|
|
4551
|
-
})],
|
|
4579
|
+
})], Q.prototype, "footerData", void 0), K([s({ attribute: !1 })], Q.prototype, "stylesheets", void 0), K([s({
|
|
4552
4580
|
type: Number,
|
|
4553
4581
|
attribute: "max-undo-size"
|
|
4554
|
-
})],
|
|
4555
|
-
function
|
|
4582
|
+
})], Q.prototype, "maxUndoSize", null), K([c()], Q.prototype, "_scrollTop", void 0), K([c()], Q.prototype, "_scrollLeft", void 0), K([c()], Q.prototype, "_viewportHeight", void 0), K([c()], Q.prototype, "_viewportWidth", void 0), K([c()], Q.prototype, "_activeCell", void 0), K([c()], Q.prototype, "_editingCell", void 0), K([c()], Q.prototype, "_sortCriteria", void 0), K([c()], Q.prototype, "_openFilterKey", void 0), K([c()], Q.prototype, "_rowSelectionVersion", void 0), K([c()], Q.prototype, "_autocompleteState", void 0), K([c()], Q.prototype, "_headerMenu", void 0), K([c()], Q.prototype, "_bodyContextMenu", void 0), K([c()], Q.prototype, "_commentPopup", void 0), K([c()], Q.prototype, "_isDragOver", void 0), Q = K([o("flex-table")], Q);
|
|
4583
|
+
function Xe(e, t) {
|
|
4556
4584
|
if (t) {
|
|
4557
4585
|
let t = ce(e);
|
|
4558
4586
|
return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(t) ? t.replace(" ", "T") : void 0;
|
|
@@ -4560,7 +4588,7 @@ function Je(e, t) {
|
|
|
4560
4588
|
let n = se(e);
|
|
4561
4589
|
return /^\d{4}-\d{2}-\d{2}$/.test(n) ? n : void 0;
|
|
4562
4590
|
}
|
|
4563
|
-
function
|
|
4591
|
+
function $(e) {
|
|
4564
4592
|
try {
|
|
4565
4593
|
return e.matches(":state(open)");
|
|
4566
4594
|
} catch {
|
|
@@ -4568,4 +4596,4 @@ function Ye(e) {
|
|
|
4568
4596
|
}
|
|
4569
4597
|
}
|
|
4570
4598
|
//#endregion
|
|
4571
|
-
export { j as a, M as i, G as n, k as o, V as r, C as s,
|
|
4599
|
+
export { j as a, M as i, G as n, k as o, V as r, C as s, Q as t };
|
package/dist/flex-table.d.ts
CHANGED
|
@@ -393,9 +393,20 @@ export declare class FlexTable extends LitElement {
|
|
|
393
393
|
/** Widen (positive) or narrow (negative) a visible column by `delta` px, never below its minimum. */
|
|
394
394
|
private _resizeColumnBy;
|
|
395
395
|
private _handleNavigation;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
/**
|
|
397
|
+
* The selection as TSV in display (sorted) order, or `null` without a selection.
|
|
398
|
+
*/
|
|
399
|
+
private _selectionTsv;
|
|
400
|
+
/**
|
|
401
|
+
* The browser's copy / cut / paste on the grid. Events from a text field inside the grid (the cell
|
|
402
|
+
* editor, the find panel, a filter input) are that field's own and pass through untouched.
|
|
403
|
+
*/
|
|
404
|
+
private _onClipboard;
|
|
405
|
+
/** The context menu's Copy — no clipboard event to fill, so it goes through the async Clipboard API. */
|
|
406
|
+
private _copyFromMenu;
|
|
407
|
+
/** Runs once the text is on the clipboard — only then is a cut range cleared. */
|
|
408
|
+
private _afterCopy;
|
|
409
|
+
private _pasteText;
|
|
399
410
|
/**
|
|
400
411
|
* After a paste, the cells it wrote are the selection — as in spreadsheets — so the reader sees
|
|
401
412
|
* what changed and can copy, clear or undo it as one block. The active cell stays where the
|
|
@@ -492,6 +503,12 @@ export declare class FlexTable extends LitElement {
|
|
|
492
503
|
private _numberFilterState;
|
|
493
504
|
private _dateFilterState;
|
|
494
505
|
private _emptyFilterState;
|
|
506
|
+
/**
|
|
507
|
+
* From/to bounds as two `u-date-picker`s — the cell editor's control, for the same reason: the native
|
|
508
|
+
* date inputs show the browser's UI language (`10/02/2026` in an English browser) while the table
|
|
509
|
+
* shows ISO. Two pickers, not one `u-date-range-picker`: either bound may be left open ("from this
|
|
510
|
+
* day on"), which a range value cannot say. Each bound's calendar stops at the other bound.
|
|
511
|
+
*/
|
|
495
512
|
private _renderDateFilter;
|
|
496
513
|
private _applyDateFilter;
|
|
497
514
|
private _renderBooleanFilter;
|
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-B8ehfxqn.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 };
|
package/dist/odata/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SortCriteria } from '../core/sorting.js';
|
|
2
|
+
import type { SourceError } from '../core/source-error.js';
|
|
2
3
|
export interface UseODataSourceOptions {
|
|
3
4
|
pageSize?: number;
|
|
4
5
|
defaultOrderBy?: string;
|
|
@@ -43,7 +44,11 @@ export interface UseODataSourceResult<T> {
|
|
|
43
44
|
data: T[];
|
|
44
45
|
totalCount: number;
|
|
45
46
|
loading: boolean;
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* 마지막 요청의 실패, 없으면 `null`. 화면에는 `error.message` 를 그린다. «어떤 실패인가» 는
|
|
49
|
+
* `status`(HTTP) · `code`(서버의 거절 코드) · `details` 로 가른다 — 401 은 `onUnauthorized` 도 부른다.
|
|
50
|
+
*/
|
|
51
|
+
error: SourceError | null;
|
|
47
52
|
page: number;
|
|
48
53
|
setPage: (page: number) => void;
|
|
49
54
|
sortCriteria: SortCriteria[];
|
package/dist/react.d.ts
CHANGED
|
@@ -69,3 +69,4 @@ export { useODataSource } from './odata/use-odata-source.js';
|
|
|
69
69
|
export type { UseODataSourceOptions, UseODataSourceResult } from './odata/types.js';
|
|
70
70
|
export { useArraySource } from './array/use-array-source.js';
|
|
71
71
|
export type { UseArraySourceOptions, UseArraySourceResult } from './array/types.js';
|
|
72
|
+
export type { SourceError, SourceErrorDetail } from './core/source-error.js';
|
package/dist/react.js
CHANGED
|
@@ -1,47 +1,74 @@
|
|
|
1
|
-
import { t as e } from "./flex-table-
|
|
1
|
+
import { t as e } from "./flex-table-B8ehfxqn.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";
|
|
5
5
|
import { createComponent as u } from "@lit/react";
|
|
6
|
+
//#region src/core/source-error.ts
|
|
7
|
+
async function d(e) {
|
|
8
|
+
let t = await e.text().catch(() => ""), n = {
|
|
9
|
+
message: `Request failed (${e.status})`,
|
|
10
|
+
status: e.status
|
|
11
|
+
};
|
|
12
|
+
if (!t) return n;
|
|
13
|
+
let r = t;
|
|
14
|
+
try {
|
|
15
|
+
r = JSON.parse(t);
|
|
16
|
+
} catch {}
|
|
17
|
+
if (n.body = r, !r || typeof r != "object") return n;
|
|
18
|
+
let i = r, a = i.error && typeof i.error == "object" ? i.error : i;
|
|
19
|
+
typeof a.message == "string" && a.message ? n.message = a.message : typeof i.message == "string" && i.message && (n.message = i.message), typeof a.code == "string" && a.code && (n.code = a.code);
|
|
20
|
+
let o = f(a.details);
|
|
21
|
+
return o && (n.details = o), n;
|
|
22
|
+
}
|
|
23
|
+
function f(e) {
|
|
24
|
+
if (!Array.isArray(e)) return;
|
|
25
|
+
let t = e.filter((e) => {
|
|
26
|
+
if (!e || typeof e != "object") return !1;
|
|
27
|
+
let t = e;
|
|
28
|
+
return typeof t.code == "string" && typeof t.message == "string";
|
|
29
|
+
});
|
|
30
|
+
return t.length > 0 ? t : void 0;
|
|
31
|
+
}
|
|
32
|
+
var p = class extends Error {
|
|
33
|
+
constructor(e) {
|
|
34
|
+
super(e.message), this.name = "SourceRequestError", this.failure = e;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function m(e) {
|
|
38
|
+
return e instanceof p ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
6
41
|
//#region src/odata/use-odata-source.ts
|
|
7
|
-
function
|
|
8
|
-
let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch:
|
|
42
|
+
function h(e, r = {}) {
|
|
43
|
+
let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: f = "", initialSort: h, fixedFilter: g, baseUrl: _, fetcher: v = fetch, onUnauthorized: y, enabled: b = !0 } = r, x = g ? JSON.stringify(g) : "", [S, C] = l([]), [w, T] = l(0), [E, D] = l(!1), [O, k] = l(null), A = t({
|
|
9
44
|
defaultOrderBy: s,
|
|
10
45
|
initialPage: u,
|
|
11
|
-
initialSearch:
|
|
12
|
-
initialSort:
|
|
13
|
-
}), [
|
|
14
|
-
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
}, []),
|
|
18
|
-
|
|
19
|
-
}, []),
|
|
46
|
+
initialSearch: f,
|
|
47
|
+
initialSort: h
|
|
48
|
+
}), [j, M] = l(A.page), [N, P] = l(A.sortCriteria), [F, I] = l(A.search), [L, R] = l(0), [z, B] = l(x);
|
|
49
|
+
x !== z && (B(x), M(0));
|
|
50
|
+
let V = c(null), H = a(() => {
|
|
51
|
+
R((e) => e + 1);
|
|
52
|
+
}, []), U = a((e) => {
|
|
53
|
+
I(e), M(0);
|
|
54
|
+
}, []), W = a((e) => {
|
|
20
55
|
let t = e.detail?.criteria;
|
|
21
|
-
t && (
|
|
56
|
+
t && (P(t), M(0));
|
|
22
57
|
}, []);
|
|
23
58
|
return o(() => {
|
|
24
|
-
if (
|
|
59
|
+
if (V.current?.abort(), !b) return;
|
|
25
60
|
let t = new AbortController();
|
|
26
|
-
|
|
61
|
+
V.current = t, D(!0), k(null);
|
|
27
62
|
let r = n({
|
|
28
|
-
page:
|
|
63
|
+
page: j,
|
|
29
64
|
pageSize: i,
|
|
30
|
-
sortCriteria:
|
|
65
|
+
sortCriteria: N,
|
|
31
66
|
defaultOrderBy: s,
|
|
32
|
-
search:
|
|
33
|
-
fixedFilter:
|
|
34
|
-
}), a = `${
|
|
35
|
-
let n = await
|
|
36
|
-
if (!n.ok)
|
|
37
|
-
n.status === 401 && g && g(n);
|
|
38
|
-
let e = await n.text().catch(() => ""), t = `Request failed (${n.status})`;
|
|
39
|
-
try {
|
|
40
|
-
let n = JSON.parse(e);
|
|
41
|
-
t = n?.error?.message ?? n?.message ?? t;
|
|
42
|
-
} catch {}
|
|
43
|
-
throw Error(t);
|
|
44
|
-
}
|
|
67
|
+
search: F,
|
|
68
|
+
fixedFilter: g
|
|
69
|
+
}), a = `${_ ?? window.location.origin}${e}${r}`, o = async (e) => {
|
|
70
|
+
let n = await v(e, { signal: t.signal });
|
|
71
|
+
if (!n.ok) throw n.status === 401 && y && y(n), new p(await d(n));
|
|
45
72
|
return n.json();
|
|
46
73
|
};
|
|
47
74
|
return (async () => {
|
|
@@ -61,42 +88,42 @@ function d(e, r = {}) {
|
|
|
61
88
|
})().then(({ count: e, rows: n }) => {
|
|
62
89
|
if (t.signal.aborted) return;
|
|
63
90
|
let r = e ?? 0;
|
|
64
|
-
|
|
91
|
+
C(n), T(r), k(null);
|
|
65
92
|
let a = r === 0 ? 0 : Math.ceil(r / i) - 1;
|
|
66
|
-
|
|
93
|
+
j > a && M(a);
|
|
67
94
|
}).catch((e) => {
|
|
68
|
-
t.signal.aborted || e.name !== "AbortError" && (
|
|
95
|
+
t.signal.aborted || e.name !== "AbortError" && (k(m(e)), C([]), T(0));
|
|
69
96
|
}).finally(() => {
|
|
70
|
-
t.signal.aborted ||
|
|
97
|
+
t.signal.aborted || D(!1);
|
|
71
98
|
}), () => t.abort();
|
|
72
99
|
}, [
|
|
73
|
-
|
|
100
|
+
b,
|
|
74
101
|
e,
|
|
75
|
-
|
|
102
|
+
j,
|
|
76
103
|
i,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
N,
|
|
105
|
+
F,
|
|
106
|
+
x,
|
|
80
107
|
s,
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
L,
|
|
109
|
+
_
|
|
83
110
|
]), {
|
|
84
|
-
data:
|
|
85
|
-
totalCount:
|
|
86
|
-
loading:
|
|
87
|
-
error:
|
|
88
|
-
page:
|
|
89
|
-
setPage:
|
|
90
|
-
sortCriteria:
|
|
91
|
-
onSortChange:
|
|
92
|
-
search:
|
|
93
|
-
setSearch:
|
|
94
|
-
refresh:
|
|
111
|
+
data: S,
|
|
112
|
+
totalCount: w,
|
|
113
|
+
loading: E || !b,
|
|
114
|
+
error: O,
|
|
115
|
+
page: j,
|
|
116
|
+
setPage: M,
|
|
117
|
+
sortCriteria: N,
|
|
118
|
+
onSortChange: W,
|
|
119
|
+
search: F,
|
|
120
|
+
setSearch: U,
|
|
121
|
+
refresh: H
|
|
95
122
|
};
|
|
96
123
|
}
|
|
97
124
|
//#endregion
|
|
98
125
|
//#region src/array/use-array-source.ts
|
|
99
|
-
function
|
|
126
|
+
function g(e, n = {}) {
|
|
100
127
|
let { pageSize: i = 20, defaultOrderBy: o, initialPage: c = 0, initialSearch: u = "", initialSort: d, columns: f, searchFields: p } = n, m = t({
|
|
101
128
|
defaultOrderBy: o,
|
|
102
129
|
initialPage: c,
|
|
@@ -137,7 +164,7 @@ function f(e, n = {}) {
|
|
|
137
164
|
refresh: C
|
|
138
165
|
};
|
|
139
166
|
}
|
|
140
|
-
var
|
|
167
|
+
var _ = u({
|
|
141
168
|
tagName: "flex-table",
|
|
142
169
|
elementClass: e,
|
|
143
170
|
react: i,
|
|
@@ -176,4 +203,4 @@ var p = u({
|
|
|
176
203
|
}
|
|
177
204
|
});
|
|
178
205
|
//#endregion
|
|
179
|
-
export {
|
|
206
|
+
export { _ as FlexTableReact, g as useArraySource, h as useODataSource };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/flex-table",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "A minimalist, input-centric data grid web component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/flex-table.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"build": "npm run typecheck && vite build && tsc --emitDeclarationOnly",
|
|
46
46
|
"build:demo": "vite build --config vite.config.demo.ts",
|
|
47
47
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
48
|
+
"pretest": "node -e \"if(require('fs').existsSync('../../scripts/sibling-dist-check.js'))require('child_process').execFileSync('node',['../../scripts/sibling-dist-check.js'],{stdio:'inherit'})\"",
|
|
48
49
|
"test": "vitest run",
|
|
49
50
|
"test:watch": "vitest",
|
|
50
51
|
"lint": "eslint src/",
|
|
@@ -127,7 +127,7 @@ Server paging from OData or an in-memory array uses the same binding — see `re
|
|
|
127
127
|
- Styles for elements returned by `render` must be passed through the `stylesheets` property
|
|
128
128
|
(constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
|
|
129
129
|
- `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
|
|
130
|
-
- Always render `error` from `useODataSource`; a failed request otherwise leaves the grid empty.
|
|
130
|
+
- Always render `error.message` from `useODataSource`; a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
|
|
131
131
|
|
|
132
132
|
## References
|
|
133
133
|
|
|
@@ -159,7 +159,7 @@ All are `CustomEvent`s with `bubbles: true, composed: true`. `row`/`index` are d
|
|
|
159
159
|
| `selection-change` | `{ selectedIndices, selectedRows }` |
|
|
160
160
|
| `clipboard-copy` / `clipboard-cut` | `{ range, text }` (TSV) |
|
|
161
161
|
| `clipboard-paste` | `{ changes, addedRows }` |
|
|
162
|
-
| `clipboard-error` | `{ action: 'copy'
|
|
162
|
+
| `clipboard-error` | `{ action: 'copy', error }` — the context menu's Copy could not write; no `clipboard-copy` follows |
|
|
163
163
|
| `fill-handle-apply` | `{ sourceRange, targetRange, cells }` |
|
|
164
164
|
| `find-replace` | `{ type: 'replace' \| 'replace-all', cells }` |
|
|
165
165
|
| `comment-change` | `{ dataIndex, colKey, text }` |
|
|
@@ -73,7 +73,7 @@ function Orders() {
|
|
|
73
73
|
return (
|
|
74
74
|
<>
|
|
75
75
|
<input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
|
|
76
|
-
{source.error && <p role="alert">{source.error}</p>}
|
|
76
|
+
{source.error && <p role="alert">{source.error.message}</p>}
|
|
77
77
|
<FlexTableReact<Order>
|
|
78
78
|
dataMode="server"
|
|
79
79
|
columns={columns}
|
|
@@ -111,7 +111,7 @@ function Orders() {
|
|
|
111
111
|
|---|---|
|
|
112
112
|
| `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
|
|
113
113
|
| `loading` | Request in flight |
|
|
114
|
-
| `error` | `
|
|
114
|
+
| `error` | `SourceError \| null` — `{ message, status?, code?, details?, body? }`; render `error.message`, branch on `status` / `code` (OData `error.code`) |
|
|
115
115
|
| `page`, `setPage` | Zero-based |
|
|
116
116
|
| `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
|
|
117
117
|
| `search`, `setSearch` | Literal text (resets to page 0) |
|