@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.
@@ -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 `renderer`
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
  */
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-JGaGcJTl.js";
2
- export { a as FlexTable, t as RowSelectionState, i as UndoStack, n as exportData, e as flexTableLocale, r as renderCell };
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';
@@ -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
- /** Display header text */
51
- header: string;
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 the header label (default: 'start').
64
- * Independent of cell content alignment — a consumer centering cell content
65
- * via a custom `renderer` (e.g. an icon-only action column) sets this to
66
- * 'center' so the header visually matches.
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?: 'start' | 'center' | 'end';
80
+ headerAlign?: ColumnAlign;
69
81
  /** Custom cell renderer — overrides built-in type rendering */
70
- renderer?: CellRenderer<T>;
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 */
@@ -1,2 +1,2 @@
1
- export { useODataSource, buildSearchExpression, parseOrderBy } from './use-odata-source.js';
2
- export type { UseODataSourceOptions, UseODataSourceResult } from './types.js';
1
+ export { buildSearchExpression, parseOrderBy, buildODataQuery } from './query.js';
2
+ export type { ODataQueryState } from './query.js';
@@ -1,2 +1,2 @@
1
- import { i as e, n as t, t as n } from "../use-odata-source-BiZrIEVV.js";
2
- export { n as buildSearchExpression, t as parseOrderBy, e as useODataSource };
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`, `renderer`/`editor`/`validator`
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', header: 'ID' },
53
- * { key: 'total', header: 'Total', renderer: (v, row) => `${row.total} ${row.currency}` },
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-JGaGcJTl.js";
2
- import t from "react";
3
- import { createComponent as n } from "@lit/react";
4
- var r = n({
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: t,
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 { r as FlexTableReact };
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.39.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.27.0",
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