@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.
- package/CHANGELOG.md +60 -0
- package/README.md +49 -34
- package/dist/array/index.d.ts +2 -3
- package/dist/array/index.js +2 -56
- package/dist/array/use-array-source.d.ts +1 -24
- package/dist/array/view.d.ts +24 -0
- package/dist/{flex-table-JGaGcJTl.js → flex-table-Dl5CYn-V.js} +174 -162
- package/dist/flex-table.d.ts +1 -1
- package/dist/flex-table.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/models/types.d.ts +20 -8
- package/dist/odata/index.d.ts +2 -2
- package/dist/odata/index.js +2 -2
- package/dist/odata/query.d.ts +72 -0
- package/dist/odata/use-odata-source.d.ts +0 -51
- package/dist/query-Dw4iO0ir.js +37 -0
- package/dist/react-events.test.d.ts +1 -0
- package/dist/react.d.ts +15 -4
- package/dist/react.js +150 -7
- package/dist/view-DiLr-TdY.js +13 -0
- 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/use-odata-source-BiZrIEVV.js +0 -118
package/dist/flex-table.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export declare class FlexTable extends LitElement {
|
|
|
88
88
|
/**
|
|
89
89
|
* Constructable stylesheets adopted into this element's shadow root, in addition to the
|
|
90
90
|
* grid's own styles. Cell renderers run inside the shadow root, so external document
|
|
91
|
-
* stylesheets (class-based utilities, design-system CSS) don't reach elements a `
|
|
91
|
+
* stylesheets (class-based utilities, design-system CSS) don't reach elements a `render` function
|
|
92
92
|
* returns — this is the escape hatch for that. Not an attribute (a `CSSStyleSheet` can't be
|
|
93
93
|
* serialized to one) — set it as a property.
|
|
94
94
|
*/
|
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,
|
|
2
|
-
export {
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-Dl5CYn-V.js";
|
|
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/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { FlexTable } from './flex-table.js';
|
|
2
|
-
export type { ColumnDefinition, ColumnType, DataRow, CellRenderer, CellEditor, CellValidator, SelectionMode, DataMode, CellStyle, ConditionalRule } from './models/types.js';
|
|
2
|
+
export type { ColumnDefinition, ColumnType, ColumnAlign, DataRow, CellRenderer, CellEditor, CellValidator, SelectionMode, DataMode, CellStyle, ConditionalRule } from './models/types.js';
|
|
3
|
+
export { effectiveAlign } from './models/types.js';
|
|
3
4
|
export type { CellPosition, CellRange } from './core/selection.js';
|
|
4
5
|
export type { SortCriteria, SortDirection } from './core/sorting.js';
|
|
5
6
|
export type { ColumnFilter, FilterPredicate, FilterErrorCallback } from './core/filtering.js';
|
package/dist/models/types.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { TemplateResult } from 'lit';
|
|
|
4
4
|
* Any string is accepted as a type — unknown types fall back to 'text' behavior.
|
|
5
5
|
*/
|
|
6
6
|
export type ColumnType = 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | (string & {});
|
|
7
|
+
/** Logical horizontal alignment — `start`/`end` follow the writing direction. */
|
|
8
|
+
export type ColumnAlign = 'start' | 'center' | 'end';
|
|
9
|
+
/** The alignment a column's cells render with: `align`, or the default derived from `type`. */
|
|
10
|
+
export declare function effectiveAlign(col: {
|
|
11
|
+
align?: ColumnAlign;
|
|
12
|
+
type?: ColumnType;
|
|
13
|
+
}): ColumnAlign;
|
|
7
14
|
/** Option item for select columns */
|
|
8
15
|
export interface SelectOption {
|
|
9
16
|
label: string;
|
|
@@ -47,8 +54,8 @@ export type DataMode = 'client' | 'server';
|
|
|
47
54
|
export interface ColumnDefinition<T = DataRow> {
|
|
48
55
|
/** Unique key matching data property names */
|
|
49
56
|
key: string;
|
|
50
|
-
/**
|
|
51
|
-
|
|
57
|
+
/** Column header text */
|
|
58
|
+
label: string;
|
|
52
59
|
/** Data type for rendering/editing (default: 'text'). Unknown types fall back to 'text'. */
|
|
53
60
|
type?: ColumnType;
|
|
54
61
|
/** Column width in pixels (default: auto) */
|
|
@@ -60,14 +67,19 @@ export interface ColumnDefinition<T = DataRow> {
|
|
|
60
67
|
/** Whether the column is sortable (default: true) */
|
|
61
68
|
sortable?: boolean;
|
|
62
69
|
/**
|
|
63
|
-
* Horizontal alignment of
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
* Horizontal alignment of cell content. Logical values, so right-to-left locales mirror.
|
|
71
|
+
* Default: derived from `type` — 'end' for 'number', 'center' for 'boolean', 'start' otherwise.
|
|
72
|
+
*/
|
|
73
|
+
align?: ColumnAlign;
|
|
74
|
+
/**
|
|
75
|
+
* Horizontal alignment of the header label.
|
|
76
|
+
* Default: the column's effective cell alignment (`align`, or the `type` default above), so a
|
|
77
|
+
* right-aligned number column gets a right-aligned header. Set it only when the header should
|
|
78
|
+
* differ from its values.
|
|
67
79
|
*/
|
|
68
|
-
headerAlign?:
|
|
80
|
+
headerAlign?: ColumnAlign;
|
|
69
81
|
/** Custom cell renderer — overrides built-in type rendering */
|
|
70
|
-
|
|
82
|
+
render?: CellRenderer<T>;
|
|
71
83
|
/** Whether the column is editable (default: true — follows global editable setting) */
|
|
72
84
|
editable?: boolean;
|
|
73
85
|
/** Custom cell editor — overrides built-in type editing */
|
package/dist/odata/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { buildSearchExpression, parseOrderBy, buildODataQuery } from './query.js';
|
|
2
|
+
export type { ODataQueryState } from './query.js';
|
package/dist/odata/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { n as
|
|
1
|
+
import { n as e, r as t, t as n } from "../query-Dw4iO0ir.js";
|
|
2
|
+
export { n as buildODataQuery, e as buildSearchExpression, t as parseOrderBy };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SortCriteria } from '../core/sorting.js';
|
|
2
|
+
/**
|
|
3
|
+
* 검색어를 OData `$search` 표현식으로 인코딩한다 (`red shirt` → `"red" AND "shirt"`).
|
|
4
|
+
*
|
|
5
|
+
* 토큰을 인용하는 이유: OData 4.0의 `searchWord`는 문자(Unicode L/Nl)만 허용해
|
|
6
|
+
* `2026`·`ZT-E2E-A` 같은 검색어가 거부된다. 4.01이 숫자·하이픈을 허용하도록 완화했으나
|
|
7
|
+
* Microsoft.OData 렉서는 아직 4.0 규칙이다(odata.net#2445). `searchPhrase`는 두 버전
|
|
8
|
+
* 모두에서 적법하므로 서버 버전과 무관하게 안전하다.
|
|
9
|
+
*
|
|
10
|
+
* 통째로가 아니라 토큰별로 감싸는 이유: 인용 없는 다중 단어는 암묵 AND로 파싱되므로
|
|
11
|
+
* (`searchAndExpr = RWS [ 'AND' RWS ] searchExpr`), 전체를 한 phrase로 감싸면 연속
|
|
12
|
+
* 문자열 매칭으로 의미가 바뀐다. 토큰별 인용은 기존 의미론을 그대로 보존한다.
|
|
13
|
+
*
|
|
14
|
+
* `"`는 phrase 안에 넣을 수 없고 이스케이프 규칙도 없어(`qchar-no-AMP-DQUOTE`) 제거한다.
|
|
15
|
+
*
|
|
16
|
+
* @returns `$search` 표현식, 또는 유효 토큰이 없으면 `undefined`
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildSearchExpression(term: string): string | undefined;
|
|
19
|
+
/** `resolveInitialState`가 읽는 옵션 — 두 소스 훅의 옵션 타입이 공통으로 갖는 부분. */
|
|
20
|
+
export interface InitialSourceStateOptions {
|
|
21
|
+
defaultOrderBy?: string;
|
|
22
|
+
initialPage?: number;
|
|
23
|
+
initialSearch?: string;
|
|
24
|
+
initialSort?: SortCriteria[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 옵션에서 `page`/`search`/`sortCriteria`의 **초기값**을 뽑는다.
|
|
28
|
+
*
|
|
29
|
+
* ★**두 훅이 이 함수 하나를 공유하는 것이 요점이다.** README가 *"same shape … so the same
|
|
30
|
+
* binding code works with either source"*를 계약으로 선언하는데, 초기값 해석을 양쪽에
|
|
31
|
+
* 복제하면 그 계약이 **문장으로만** 유지된다 — 이 리포가 반복 기록한 실패 형태다.
|
|
32
|
+
* 구현이 하나면 드리프트가 없다.
|
|
33
|
+
*
|
|
34
|
+
* ⚠**`initialSort`가 `defaultOrderBy`를 이긴다.** 둘은 같은 것을 서로 다른 표기로
|
|
35
|
+
* 말하고(`SortCriteria[]` ↔ `$orderby` 문자열), 더 구체적인 쪽을 우선한다.
|
|
36
|
+
* `initialSort`는 `onSortChange`가 주는 모양 그대로라 저장해 둔 정렬을 파싱 없이 되돌린다.
|
|
37
|
+
*
|
|
38
|
+
* ⚠**React가 useState 초기값을 첫 렌더에서만 읽는다는 사실이 계약의 일부다** — 이후의
|
|
39
|
+
* 옵션 변경은 무시되고, 이동은 `setPage`/`setSearch`로 한다. `defaultOrderBy`가 이미
|
|
40
|
+
* 그렇게 동작해 왔으므로 새 규칙이 아니다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveInitialState(options: InitialSourceStateOptions): {
|
|
43
|
+
page: number;
|
|
44
|
+
search: string;
|
|
45
|
+
sortCriteria: SortCriteria[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* `$orderby` 문자열을 정렬 기준 배열로 파싱한다 (`'a asc, b desc'`).
|
|
49
|
+
* 방향이 생략되거나 `desc`가 아니면 `asc`로 본다(OData 기본값).
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseOrderBy(orderBy: string): SortCriteria[];
|
|
52
|
+
/** `buildODataQuery` 가 읽는 표 상태 — `useODataSource` 가 요청마다 넘기는 것과 같은 모양. */
|
|
53
|
+
export interface ODataQueryState {
|
|
54
|
+
/** 0부터 세는 페이지 번호. */
|
|
55
|
+
page: number;
|
|
56
|
+
pageSize: number;
|
|
57
|
+
sortCriteria?: SortCriteria[];
|
|
58
|
+
/** 정렬 기준이 비었을 때 쓰는 `$orderby` 문자열. */
|
|
59
|
+
defaultOrderBy?: string;
|
|
60
|
+
/** 사용자가 친 검색어(리터럴) — `buildSearchExpression` 으로 인코딩된다. */
|
|
61
|
+
search?: string;
|
|
62
|
+
/** odata-query 의 필터 객체(`{ IsActive: true }` 등). */
|
|
63
|
+
fixedFilter?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 표 상태를 OData 쿼리 문자열(`?$top=…&$skip=…&$count=true…`)로 만든다.
|
|
67
|
+
*
|
|
68
|
+
* `useODataSource` 가 요청마다 하는 일 그대로다 — 훅 밖으로 꺼낸 이유는 `buildSearchExpression` 과
|
|
69
|
+
* 같다: React 없이 서버 페이징을 하는 소비자(Lit 앱의 표, 표가 아닌 목록)가 같은 쿼리를
|
|
70
|
+
* 재구현하지 않게. 정렬 기준이 있으면 그것이, 없으면 `defaultOrderBy` 가 `$orderby` 가 된다.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildODataQuery(state: ODataQueryState): string;
|
|
@@ -1,57 +1,6 @@
|
|
|
1
|
-
import type { SortCriteria } from '../core/sorting.js';
|
|
2
1
|
import type { UseODataSourceOptions, UseODataSourceResult } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* 검색어를 OData `$search` 표현식으로 인코딩한다 (`red shirt` → `"red" AND "shirt"`).
|
|
5
|
-
*
|
|
6
|
-
* 토큰을 인용하는 이유: OData 4.0의 `searchWord`는 문자(Unicode L/Nl)만 허용해
|
|
7
|
-
* `2026`·`ZT-E2E-A` 같은 검색어가 거부된다. 4.01이 숫자·하이픈을 허용하도록 완화했으나
|
|
8
|
-
* Microsoft.OData 렉서는 아직 4.0 규칙이다(odata.net#2445). `searchPhrase`는 두 버전
|
|
9
|
-
* 모두에서 적법하므로 서버 버전과 무관하게 안전하다.
|
|
10
|
-
*
|
|
11
|
-
* 통째로가 아니라 토큰별로 감싸는 이유: 인용 없는 다중 단어는 암묵 AND로 파싱되므로
|
|
12
|
-
* (`searchAndExpr = RWS [ 'AND' RWS ] searchExpr`), 전체를 한 phrase로 감싸면 연속
|
|
13
|
-
* 문자열 매칭으로 의미가 바뀐다. 토큰별 인용은 기존 의미론을 그대로 보존한다.
|
|
14
|
-
*
|
|
15
|
-
* `"`는 phrase 안에 넣을 수 없고 이스케이프 규칙도 없어(`qchar-no-AMP-DQUOTE`) 제거한다.
|
|
16
|
-
*
|
|
17
|
-
* @returns `$search` 표현식, 또는 유효 토큰이 없으면 `undefined`
|
|
18
|
-
*/
|
|
19
|
-
export declare function buildSearchExpression(term: string): string | undefined;
|
|
20
|
-
/** `resolveInitialState`가 읽는 옵션 — 두 소스 훅의 옵션 타입이 공통으로 갖는 부분. */
|
|
21
|
-
export interface InitialSourceStateOptions {
|
|
22
|
-
defaultOrderBy?: string;
|
|
23
|
-
initialPage?: number;
|
|
24
|
-
initialSearch?: string;
|
|
25
|
-
initialSort?: SortCriteria[];
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* 옵션에서 `page`/`search`/`sortCriteria`의 **초기값**을 뽑는다.
|
|
29
|
-
*
|
|
30
|
-
* ★**두 훅이 이 함수 하나를 공유하는 것이 요점이다.** README가 *"same shape … so the same
|
|
31
|
-
* binding code works with either source"*를 계약으로 선언하는데, 초기값 해석을 양쪽에
|
|
32
|
-
* 복제하면 그 계약이 **문장으로만** 유지된다 — 이 리포가 반복 기록한 실패 형태다.
|
|
33
|
-
* 구현이 하나면 드리프트가 없다.
|
|
34
|
-
*
|
|
35
|
-
* ⚠**`initialSort`가 `defaultOrderBy`를 이긴다.** 둘은 같은 것을 서로 다른 표기로
|
|
36
|
-
* 말하고(`SortCriteria[]` ↔ `$orderby` 문자열), 더 구체적인 쪽을 우선한다.
|
|
37
|
-
* `initialSort`는 `onSortChange`가 주는 모양 그대로라 저장해 둔 정렬을 파싱 없이 되돌린다.
|
|
38
|
-
*
|
|
39
|
-
* ⚠**React가 useState 초기값을 첫 렌더에서만 읽는다는 사실이 계약의 일부다** — 이후의
|
|
40
|
-
* 옵션 변경은 무시되고, 이동은 `setPage`/`setSearch`로 한다. `defaultOrderBy`가 이미
|
|
41
|
-
* 그렇게 동작해 왔으므로 새 규칙이 아니다.
|
|
42
|
-
*/
|
|
43
|
-
export declare function resolveInitialState(options: InitialSourceStateOptions): {
|
|
44
|
-
page: number;
|
|
45
|
-
search: string;
|
|
46
|
-
sortCriteria: SortCriteria[];
|
|
47
|
-
};
|
|
48
2
|
/**
|
|
49
3
|
* OData v4 서버 사이드 데이터소스 React 훅.
|
|
50
4
|
* flex-table의 dataMode="server"와 함께 사용한다.
|
|
51
5
|
*/
|
|
52
6
|
export declare function useODataSource<T = Record<string, unknown>>(url: string, options?: UseODataSourceOptions): UseODataSourceResult<T>;
|
|
53
|
-
/**
|
|
54
|
-
* `$orderby` 문자열을 정렬 기준 배열로 파싱한다 (`'a asc, b desc'`).
|
|
55
|
-
* 방향이 생략되거나 `desc`가 아니면 `asc`로 본다(OData 기본값).
|
|
56
|
-
*/
|
|
57
|
-
export declare function parseOrderBy(orderBy: string): SortCriteria[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import e from "odata-query";
|
|
2
|
+
//#region src/odata/query.ts
|
|
3
|
+
function t(e) {
|
|
4
|
+
let t = e.split(/\s+/).map((e) => e.replace(/"/g, "")).filter((e) => e.length > 0);
|
|
5
|
+
if (t.length !== 0) return t.map((e) => `"${e}"`).join(" AND ");
|
|
6
|
+
}
|
|
7
|
+
function n(e) {
|
|
8
|
+
let { defaultOrderBy: t, initialPage: n = 0, initialSearch: i = "", initialSort: a } = e;
|
|
9
|
+
return {
|
|
10
|
+
page: n,
|
|
11
|
+
search: i,
|
|
12
|
+
sortCriteria: a ?? (t ? r(t) : [])
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function r(e) {
|
|
16
|
+
return e.split(",").map((e) => {
|
|
17
|
+
let t = e.trim().split(/\s+/);
|
|
18
|
+
return {
|
|
19
|
+
key: t[0],
|
|
20
|
+
direction: t[1]?.toLowerCase() === "desc" ? "desc" : "asc"
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function i(n) {
|
|
25
|
+
let { page: r, pageSize: i, sortCriteria: a = [], defaultOrderBy: o, search: s, fixedFilter: c } = n, l = a.length > 0 ? a.map((e) => `${e.key} ${e.direction}`).join(", ") : o, u = {
|
|
26
|
+
top: i,
|
|
27
|
+
skip: r * i,
|
|
28
|
+
count: !0
|
|
29
|
+
};
|
|
30
|
+
if (l && (u.orderBy = l), c && (u.filter = c), s) {
|
|
31
|
+
let e = t(s);
|
|
32
|
+
e && (u.search = e);
|
|
33
|
+
}
|
|
34
|
+
return e(u);
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { n as i, t as n, r, i as t };
|
|
@@ -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
|
/**
|
|
@@ -43,18 +50,22 @@ export type FlexTableReactProps<T = DataRow> = Omit<BaseProps, 'data' | 'columns
|
|
|
43
50
|
* The underlying custom element (`FlexTable`) is a single registered class and cannot
|
|
44
51
|
* itself be generic across instances — the DOM has no notion of `FlexTable<Order>` vs
|
|
45
52
|
* `FlexTable<Consumer>`. `FlexTableReact<T>` performs one internal cast at this boundary
|
|
46
|
-
* so consumers get end-to-end type safety (`data`, `columns`, `
|
|
53
|
+
* so consumers get end-to-end type safety (`data`, `columns`, `render`/`editor`/`validator`
|
|
47
54
|
* callbacks) without casting at every call site.
|
|
48
55
|
*
|
|
49
56
|
* @example
|
|
50
57
|
* ```tsx
|
|
51
58
|
* const columns: ColumnDefinition<Order>[] = [
|
|
52
|
-
* { key: 'id',
|
|
53
|
-
* { key: 'total',
|
|
59
|
+
* { key: 'id', label: 'ID' },
|
|
60
|
+
* { key: 'total', label: 'Total', render: (v, row) => `${row.total} ${row.currency}` },
|
|
54
61
|
* ];
|
|
55
62
|
* <FlexTableReact<Order> data={orders} columns={columns} />
|
|
56
63
|
* ```
|
|
57
64
|
*/
|
|
58
65
|
export declare const FlexTableReact: <T = DataRow>(props: FlexTableReactProps<T> & React.RefAttributes<FlexTable>) => React.ReactElement | null;
|
|
59
66
|
export type { FlexTable };
|
|
60
|
-
export type { ColumnDefinition, DataRow, ColumnType, CellRenderer, CellEditor, CellValidator, ConditionalRule, SelectionMode, DataMode } from './models/types.js';
|
|
67
|
+
export type { ColumnDefinition, DataRow, ColumnType, ColumnAlign, CellRenderer, CellEditor, CellValidator, ConditionalRule, SelectionMode, DataMode } from './models/types.js';
|
|
68
|
+
export { useODataSource } from './odata/use-odata-source.js';
|
|
69
|
+
export type { UseODataSourceOptions, UseODataSourceResult } from './odata/types.js';
|
|
70
|
+
export { useArraySource } from './array/use-array-source.js';
|
|
71
|
+
export type { UseArraySourceOptions, UseArraySourceResult } from './array/types.js';
|
package/dist/react.js
CHANGED
|
@@ -1,10 +1,146 @@
|
|
|
1
|
-
import { t as e } from "./flex-table-
|
|
2
|
-
import t from "
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { t as e } from "./flex-table-Dl5CYn-V.js";
|
|
2
|
+
import { i as t, t as n } from "./query-Dw4iO0ir.js";
|
|
3
|
+
import { t as r } from "./view-DiLr-TdY.js";
|
|
4
|
+
import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
|
|
5
|
+
import { createComponent as u } from "@lit/react";
|
|
6
|
+
//#region src/odata/use-odata-source.ts
|
|
7
|
+
function d(e, r = {}) {
|
|
8
|
+
let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: d = "", initialSort: f, fixedFilter: p, baseUrl: m, fetcher: h = fetch, onUnauthorized: g, enabled: _ = !0 } = r, v = p ? JSON.stringify(p) : "", [y, b] = l([]), [x, S] = l(0), [C, w] = l(!1), [T, E] = l(null), D = t({
|
|
9
|
+
defaultOrderBy: s,
|
|
10
|
+
initialPage: u,
|
|
11
|
+
initialSearch: d,
|
|
12
|
+
initialSort: f
|
|
13
|
+
}), [O, k] = l(D.page), [A, j] = l(D.sortCriteria), [M, N] = l(D.search), [P, F] = l(0), [I, L] = l(v);
|
|
14
|
+
v !== I && (L(v), k(0));
|
|
15
|
+
let R = c(null), z = a(() => {
|
|
16
|
+
F((e) => e + 1);
|
|
17
|
+
}, []), B = a((e) => {
|
|
18
|
+
N(e), k(0);
|
|
19
|
+
}, []), V = a((e) => {
|
|
20
|
+
let t = e.detail?.criteria;
|
|
21
|
+
t && (j(t), k(0));
|
|
22
|
+
}, []);
|
|
23
|
+
return o(() => {
|
|
24
|
+
if (R.current?.abort(), !_) return;
|
|
25
|
+
let t = new AbortController();
|
|
26
|
+
R.current = t, w(!0), E(null);
|
|
27
|
+
let r = n({
|
|
28
|
+
page: O,
|
|
29
|
+
pageSize: i,
|
|
30
|
+
sortCriteria: A,
|
|
31
|
+
defaultOrderBy: s,
|
|
32
|
+
search: M,
|
|
33
|
+
fixedFilter: p
|
|
34
|
+
}), a = `${m ?? window.location.origin}${e}${r}`, o = async (e) => {
|
|
35
|
+
let n = await h(e, { signal: t.signal });
|
|
36
|
+
if (!n.ok) {
|
|
37
|
+
(n.status === 401 || n.status === 403) && 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
|
+
}
|
|
45
|
+
return n.json();
|
|
46
|
+
};
|
|
47
|
+
return (async () => {
|
|
48
|
+
let e = await o(a), t = [...e.value ?? e], n = new URL(a).origin, r = /* @__PURE__ */ new Set([a]), s = a, c = e["@odata.nextLink"];
|
|
49
|
+
for (; typeof c == "string" && c && t.length < i;) {
|
|
50
|
+
let e = new URL(c, s);
|
|
51
|
+
if (e.origin !== n) throw Error(`OData nextLink points outside the source origin (${e.origin})`);
|
|
52
|
+
if (s = e.toString(), r.has(s)) throw Error(`OData nextLink repeats an already-read page (${s})`);
|
|
53
|
+
r.add(s);
|
|
54
|
+
let i = await o(s);
|
|
55
|
+
t.push(...i.value ?? []), c = i["@odata.nextLink"];
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
count: e["@odata.count"],
|
|
59
|
+
rows: t
|
|
60
|
+
};
|
|
61
|
+
})().then(({ count: e, rows: n }) => {
|
|
62
|
+
if (t.signal.aborted) return;
|
|
63
|
+
let r = e ?? 0;
|
|
64
|
+
b(n), S(r), E(null);
|
|
65
|
+
let a = r === 0 ? 0 : Math.ceil(r / i) - 1;
|
|
66
|
+
O > a && k(a);
|
|
67
|
+
}).catch((e) => {
|
|
68
|
+
t.signal.aborted || e.name !== "AbortError" && (E(e.message), b([]), S(0));
|
|
69
|
+
}).finally(() => {
|
|
70
|
+
t.signal.aborted || w(!1);
|
|
71
|
+
}), () => t.abort();
|
|
72
|
+
}, [
|
|
73
|
+
_,
|
|
74
|
+
e,
|
|
75
|
+
O,
|
|
76
|
+
i,
|
|
77
|
+
A,
|
|
78
|
+
M,
|
|
79
|
+
v,
|
|
80
|
+
s,
|
|
81
|
+
P,
|
|
82
|
+
m
|
|
83
|
+
]), {
|
|
84
|
+
data: y,
|
|
85
|
+
totalCount: x,
|
|
86
|
+
loading: C || !_,
|
|
87
|
+
error: T,
|
|
88
|
+
page: O,
|
|
89
|
+
setPage: k,
|
|
90
|
+
sortCriteria: A,
|
|
91
|
+
onSortChange: V,
|
|
92
|
+
search: M,
|
|
93
|
+
setSearch: B,
|
|
94
|
+
refresh: z
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/array/use-array-source.ts
|
|
99
|
+
function f(e, n = {}) {
|
|
100
|
+
let { pageSize: i = 20, defaultOrderBy: o, initialPage: c = 0, initialSearch: u = "", initialSort: d, columns: f, searchFields: p } = n, m = t({
|
|
101
|
+
defaultOrderBy: o,
|
|
102
|
+
initialPage: c,
|
|
103
|
+
initialSearch: u,
|
|
104
|
+
initialSort: d
|
|
105
|
+
}), [h, g] = l(m.page), [_, v] = l(m.sortCriteria), [y, b] = l(m.search), x = a((e) => {
|
|
106
|
+
b(e), g(0);
|
|
107
|
+
}, []), S = a((e) => {
|
|
108
|
+
let t = e.detail?.criteria;
|
|
109
|
+
t && (v(t), g(0));
|
|
110
|
+
}, []), C = a(() => {}, []), { data: w, totalCount: T } = s(() => r(e, {
|
|
111
|
+
search: y,
|
|
112
|
+
sortCriteria: _,
|
|
113
|
+
page: h,
|
|
114
|
+
pageSize: i,
|
|
115
|
+
columns: f,
|
|
116
|
+
searchFields: p
|
|
117
|
+
}), [
|
|
118
|
+
e,
|
|
119
|
+
y,
|
|
120
|
+
_,
|
|
121
|
+
h,
|
|
122
|
+
i,
|
|
123
|
+
f,
|
|
124
|
+
p
|
|
125
|
+
]), E = T === 0 ? 0 : Math.ceil(T / i) - 1;
|
|
126
|
+
return h > E && g(E), {
|
|
127
|
+
data: w,
|
|
128
|
+
totalCount: T,
|
|
129
|
+
loading: !1,
|
|
130
|
+
error: null,
|
|
131
|
+
page: h,
|
|
132
|
+
setPage: g,
|
|
133
|
+
sortCriteria: _,
|
|
134
|
+
onSortChange: S,
|
|
135
|
+
search: y,
|
|
136
|
+
setSearch: x,
|
|
137
|
+
refresh: C
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var p = u({
|
|
5
141
|
tagName: "flex-table",
|
|
6
142
|
elementClass: e,
|
|
7
|
-
react:
|
|
143
|
+
react: i,
|
|
8
144
|
events: {
|
|
9
145
|
onCellSelect: "cell-select",
|
|
10
146
|
onCellEditCommit: "cell-edit-commit",
|
|
@@ -29,8 +165,15 @@ var r = n({
|
|
|
29
165
|
onValidationError: "validation-error",
|
|
30
166
|
onBatchUpdate: "batch-update",
|
|
31
167
|
onContextMenu: "context-menu",
|
|
32
|
-
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"
|
|
33
176
|
}
|
|
34
177
|
});
|
|
35
178
|
//#endregion
|
|
36
|
-
export {
|
|
179
|
+
export { p as FlexTableReact, f as useArraySource, d as useODataSource };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as e } from "./sorting-CjfjRxwL.js";
|
|
2
|
+
//#region src/array/view.ts
|
|
3
|
+
function t(t, { search: n, sortCriteria: r, page: i, pageSize: a, columns: o, searchFields: s }) {
|
|
4
|
+
let c = n.trim().toLowerCase(), l = c ? t.filter((e) => (s ? s(e) : Object.values(e)).some((e) => e != null && String(e).toLowerCase().includes(c))) : t, u = l;
|
|
5
|
+
r.length > 0 && (u = e(l, r, o ?? []).map((e) => l[e]));
|
|
6
|
+
let d = u.length, f = d === 0 ? 0 : Math.ceil(d / a) - 1, p = Math.min(Math.max(i, 0), f) * a;
|
|
7
|
+
return {
|
|
8
|
+
data: u.slice(p, p + a),
|
|
9
|
+
totalCount: d
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/flex-table",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
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.51.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
|