@iyulab/flex-table 0.25.0 → 0.27.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 +33 -0
- package/README.md +6 -0
- package/dist/core/row-selection.d.ts +7 -0
- package/dist/{flex-table-DZFtW02v.js → flex-table-CeT8zkEi.js} +37 -5
- package/dist/flex-table.d.ts +28 -0
- package/dist/flex-table.js +1 -1
- package/dist/models/types.d.ts +7 -0
- package/dist/react.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.27.0] - 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Shift-click range selection on the row checkbox column** (`selectable` + `selectionMode:
|
|
8
|
+
'multi'`). Clicking a row's checkbox while holding Shift now selects every row between it and
|
|
9
|
+
the last row you clicked, matching the row-selection convention used by spreadsheets and file
|
|
10
|
+
managers. A plain click still just toggles the one row it lands on, and the anchor resets to
|
|
11
|
+
whichever row you last clicked (shift or not) — the same rules as native cell selection already
|
|
12
|
+
followed elsewhere in the grid.
|
|
13
|
+
- **`selectWhere(predicate)`** — selects every currently-loaded row for which `predicate(row,
|
|
14
|
+
dataIndex)` returns true, added to whatever is already selected (call `deselectAll()` first for
|
|
15
|
+
a fresh set). Built for "the user pastes a list of keys, select the matching rows" flows: the
|
|
16
|
+
host doesn't need to reason about visual-vs-data row indices or virtualization — it gets the
|
|
17
|
+
same row objects `data` was set with.
|
|
18
|
+
- **`emptyMessage` / `noMatchingMessage`** — the text shown in the empty state (`data` is empty,
|
|
19
|
+
or every row is hidden by an active column filter) was previously hardcoded to `'No data'` /
|
|
20
|
+
`'No matching data'` with no way to override it. Any consumer localizing their UI, or wanting a
|
|
21
|
+
more specific message ("no orders yet — create one"), had to reimplement the empty state
|
|
22
|
+
themselves. Both now default to the same English strings for backward compatibility, and can be
|
|
23
|
+
overridden as string properties/attributes (`empty-message`, `no-matching-message`).
|
|
24
|
+
|
|
25
|
+
## [0.26.0] - 2026-08-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`ColumnDefinition.headerAlign`** (`'start' | 'center' | 'end'`, default `'start'`). Header
|
|
30
|
+
label alignment was previously fixed to the left regardless of how the column's own content is
|
|
31
|
+
rendered. A column whose `renderer` centers its content (an icon-only action column, for
|
|
32
|
+
example) had no way to make the header label match, so the two visually drifted apart. Setting
|
|
33
|
+
`headerAlign: 'center'` (or `'end'`) aligns the header text independently of cell content
|
|
34
|
+
alignment, which the consumer already fully controls via `renderer`.
|
|
35
|
+
|
|
3
36
|
## [0.25.0] - 2026-08-07
|
|
4
37
|
|
|
5
38
|
### Added
|
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ A lightweight, schema-agnostic data grid web component built with [Lit](https://
|
|
|
4
4
|
|
|
5
5
|
Designed for effortless data input and crystal-clear visibility. Bridges the gap between spreadsheet freedom and database structural integrity.
|
|
6
6
|
|
|
7
|
+
**`flex-table` vs. `URichTable`** (`@iyulab/data-components`): both can consume external data
|
|
8
|
+
sources such as OData — the choice is about scale and interaction, not the data source.
|
|
9
|
+
|
|
10
|
+
- **`flex-table`** — large datasets, cell-level editing, spreadsheet-grade interaction
|
|
11
|
+
- **`URichTable`** — small-to-medium datasets, row-level CRUD, selection/filter-focused UX
|
|
12
|
+
|
|
7
13
|
## Install
|
|
8
14
|
|
|
9
15
|
```bash
|
|
@@ -20,4 +20,11 @@ export declare class RowSelectionState {
|
|
|
20
20
|
deselect(index: number): void;
|
|
21
21
|
selectAll(): void;
|
|
22
22
|
deselectAll(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Selects every index between from and to, inclusive, in either direction.
|
|
25
|
+
* Used for shift-click range selection anchored at the last toggled row.
|
|
26
|
+
* Single mode: only the endpoint (to) ends up selected, matching toggle()'s
|
|
27
|
+
* "one at a time" semantics — a range doesn't make sense with one slot.
|
|
28
|
+
*/
|
|
29
|
+
selectRange(from: number, to: number): void;
|
|
23
30
|
}
|
|
@@ -153,6 +153,9 @@ var s = t`
|
|
|
153
153
|
.ft-header-cell.ft-sortable { cursor: pointer; }
|
|
154
154
|
.ft-header-cell.ft-sortable:hover { background: var(--ft-header-hover-bg); }
|
|
155
155
|
|
|
156
|
+
.ft-header-cell.ft-header-align-center { justify-content: center; }
|
|
157
|
+
.ft-header-cell.ft-header-align-end { justify-content: flex-end; }
|
|
158
|
+
|
|
156
159
|
.ft-resize-handle {
|
|
157
160
|
position: absolute;
|
|
158
161
|
right: 0; top: 0; bottom: 0;
|
|
@@ -1110,6 +1113,14 @@ var y = class {
|
|
|
1110
1113
|
deselectAll() {
|
|
1111
1114
|
this._selected.clear();
|
|
1112
1115
|
}
|
|
1116
|
+
selectRange(e, t) {
|
|
1117
|
+
if (this._mode === "single") {
|
|
1118
|
+
this.select(t);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
let n = Math.min(e, t), r = Math.max(e, t);
|
|
1122
|
+
for (let e = n; e <= r; e++) e >= 0 && e < this._rowCount && this._selected.add(e);
|
|
1123
|
+
}
|
|
1113
1124
|
};
|
|
1114
1125
|
//#endregion
|
|
1115
1126
|
//#region src/core/sorting.ts
|
|
@@ -1535,7 +1546,7 @@ async function xe(e) {
|
|
|
1535
1546
|
};
|
|
1536
1547
|
}
|
|
1537
1548
|
//#endregion
|
|
1538
|
-
//#region \0@oxc-project+runtime@0.
|
|
1549
|
+
//#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorate.js
|
|
1539
1550
|
function q(e, t, n, r) {
|
|
1540
1551
|
var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
|
|
1541
1552
|
if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
|
|
@@ -1553,7 +1564,7 @@ var J = {
|
|
|
1553
1564
|
lte: "≤"
|
|
1554
1565
|
}, Y = 120, X = 40, Z = 32, Q = 5, $ = class extends e {
|
|
1555
1566
|
constructor(...e) {
|
|
1556
|
-
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new y(), this._editing = new b(), this._rowSelection = new x(), this._undo = new D(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, 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._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._onCommentPopupOutsideClick = () => {
|
|
1567
|
+
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, 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._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new y(), this._editing = new b(), this._rowSelection = new x(), this._undo = new D(), 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._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._onCommentPopupOutsideClick = () => {
|
|
1557
1568
|
this._commitCommentPopup();
|
|
1558
1569
|
}, 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();
|
|
1559
1570
|
}
|
|
@@ -1624,6 +1635,15 @@ var J = {
|
|
|
1624
1635
|
deselectAll() {
|
|
1625
1636
|
this.selectable && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent());
|
|
1626
1637
|
}
|
|
1638
|
+
selectWhere(e) {
|
|
1639
|
+
if (this.selectable) {
|
|
1640
|
+
for (let t = 0; t < this._visibleRowCount; t++) {
|
|
1641
|
+
let n = this._toDataIndex(t), r = this.data[n];
|
|
1642
|
+
r !== void 0 && e(r, n) && this._rowSelection.select(t);
|
|
1643
|
+
}
|
|
1644
|
+
this._rowSelectionVersion++, this._dispatchRowSelectionEvent(), this.requestUpdate();
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1627
1647
|
_dispatchRowSelectionEvent() {
|
|
1628
1648
|
let { selectedIndices: e, selectedRows: t } = this.getSelectedRows();
|
|
1629
1649
|
this.dispatchEvent(new CustomEvent("selection-change", {
|
|
@@ -2793,7 +2813,9 @@ var J = {
|
|
|
2793
2813
|
let i = e.sortable !== !1, a = this._sortCriteria.find((t) => t.key === e.key), o = this._sortCriteria.length > 1 ? this._sortCriteria.findIndex((t) => t.key === e.key) : -1, s = this._filters.some((t) => t.key === e.key), c = e.pinned === "left", l = e.pinned === "right", u = [
|
|
2794
2814
|
"ft-header-cell",
|
|
2795
2815
|
i ? "ft-sortable" : "",
|
|
2796
|
-
c || l ? "ft-pinned" : ""
|
|
2816
|
+
c || l ? "ft-pinned" : "",
|
|
2817
|
+
e.headerAlign === "center" ? "ft-header-align-center" : "",
|
|
2818
|
+
e.headerAlign === "end" ? "ft-header-align-end" : ""
|
|
2797
2819
|
].filter(Boolean).join(" "), d = i ? a ? a.direction === "asc" ? "ascending" : "descending" : "none" : void 0, f = this._getColWidth(e), p = this.headerHeight, m = this._colLeftOffsets[t] ?? 0, h;
|
|
2798
2820
|
h = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : l ? `position: absolute; top: 0; right: ${-this._scrollLeft + this._getPinnedRight(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : `left: ${m}px; width: ${f}px; height: ${p}px;`;
|
|
2799
2821
|
let g = this._colDrag?.active && this._colDrag.colIndex === t, _ = [...u.split(" "), g ? "ft-col-dragging" : ""].filter(Boolean).join(" "), v = this.columns, y = v.findIndex((t) => t.key === e.key), b = [];
|
|
@@ -3789,7 +3811,7 @@ var J = {
|
|
|
3789
3811
|
for (let t = s; t < c; t++) m.push(this._renderHeaderCell(e[t], t));
|
|
3790
3812
|
let h = this._colDragIndicatorLeft == null ? "" : n`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
|
|
3791
3813
|
if (this.data.length === 0 || this._visibleRowCount === 0) {
|
|
3792
|
-
let e = this.data.length === 0 ?
|
|
3814
|
+
let e = this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage;
|
|
3793
3815
|
return n`
|
|
3794
3816
|
${i}
|
|
3795
3817
|
<div class="ft-header" role="row" style="width: ${o}px; height: ${a}px;">
|
|
@@ -3828,7 +3850,10 @@ var J = {
|
|
|
3828
3850
|
e.target.checked ? this._rowSelection.selectAll() : this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
|
|
3829
3851
|
}
|
|
3830
3852
|
_onRowCheckboxChange(e, t) {
|
|
3831
|
-
e.stopPropagation(), this._rowSelection.toggle(t), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
|
|
3853
|
+
e.stopPropagation(), this._checkboxShiftPending && this._lastCheckboxRowIndex !== null ? this._rowSelection.selectRange(this._lastCheckboxRowIndex, t) : this._rowSelection.toggle(t), this._lastCheckboxRowIndex = t, this._checkboxShiftPending = !1, this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
|
|
3854
|
+
}
|
|
3855
|
+
_onRowCheckboxClick(e) {
|
|
3856
|
+
this._checkboxShiftPending = e.shiftKey;
|
|
3832
3857
|
}
|
|
3833
3858
|
_renderFrozenRows(e, t, i) {
|
|
3834
3859
|
let a = this._frozenRowCount;
|
|
@@ -3875,6 +3900,7 @@ var J = {
|
|
|
3875
3900
|
style="position: absolute; top: 0; left: ${m + h}px; width: 36px; height: ${u}px; z-index: 2;">
|
|
3876
3901
|
<input type="checkbox"
|
|
3877
3902
|
.checked=${p}
|
|
3903
|
+
@click=${(e) => this._onRowCheckboxClick(e)}
|
|
3878
3904
|
@change=${(t) => this._onRowCheckboxChange(t, e)}>
|
|
3879
3905
|
</div>
|
|
3880
3906
|
` : "";
|
|
@@ -4050,6 +4076,12 @@ q([a({ type: Array })], $.prototype, "columns", void 0), q([a({
|
|
|
4050
4076
|
type: Number,
|
|
4051
4077
|
attribute: "frozen-rows"
|
|
4052
4078
|
})], $.prototype, "frozenRows", void 0), q([a({
|
|
4079
|
+
type: String,
|
|
4080
|
+
attribute: "empty-message"
|
|
4081
|
+
})], $.prototype, "emptyMessage", void 0), q([a({
|
|
4082
|
+
type: String,
|
|
4083
|
+
attribute: "no-matching-message"
|
|
4084
|
+
})], $.prototype, "noMatchingMessage", void 0), q([a({
|
|
4053
4085
|
type: Boolean,
|
|
4054
4086
|
reflect: !0
|
|
4055
4087
|
})], $.prototype, "loading", void 0), q([a({
|
package/dist/flex-table.d.ts
CHANGED
|
@@ -61,6 +61,13 @@ export declare class FlexTable extends LitElement {
|
|
|
61
61
|
showContextMenu: boolean;
|
|
62
62
|
/** Number of rows to freeze at the top (always visible during vertical scroll). */
|
|
63
63
|
frozenRows: number;
|
|
64
|
+
/** Message shown when `data` is empty (no rows at all). Default: 'No data'. */
|
|
65
|
+
emptyMessage: string;
|
|
66
|
+
/**
|
|
67
|
+
* Message shown when `data` has rows but every one is hidden by the active
|
|
68
|
+
* column filters (0 visible rows, non-empty `data`). Default: 'No matching data'.
|
|
69
|
+
*/
|
|
70
|
+
noMatchingMessage: string;
|
|
64
71
|
/**
|
|
65
72
|
* True일 때 그리드 위에 로딩 오버레이를 표시하고 host에 `aria-busy="true"`를 반영한다.
|
|
66
73
|
* `useODataSource()`가 반환하는 `loading`과 자연 연동하도록 설계됨:
|
|
@@ -98,6 +105,10 @@ export declare class FlexTable extends LitElement {
|
|
|
98
105
|
private _sortedIndices;
|
|
99
106
|
private _openFilterKey;
|
|
100
107
|
private _rowSelectionVersion;
|
|
108
|
+
/** Anchor row for shift-click range selection on the row checkbox column. */
|
|
109
|
+
private _lastCheckboxRowIndex;
|
|
110
|
+
/** Set by the checkbox's own click (which carries shiftKey) just before its change event fires. */
|
|
111
|
+
private _checkboxShiftPending;
|
|
101
112
|
private _autocompleteState;
|
|
102
113
|
private _headerMenu;
|
|
103
114
|
private _bodyContextMenu;
|
|
@@ -138,6 +149,17 @@ export declare class FlexTable extends LitElement {
|
|
|
138
149
|
selectAll(): void;
|
|
139
150
|
/** Deselect all rows. */
|
|
140
151
|
deselectAll(): void;
|
|
152
|
+
/**
|
|
153
|
+
* Selects every currently-loaded row for which `predicate` returns true, in addition to
|
|
154
|
+
* whatever is already selected — call `deselectAll()` first for a fresh set. Iterates the
|
|
155
|
+
* visible (post filter/sort) view and resolves each visual row to its underlying data row,
|
|
156
|
+
* so the predicate always sees the same objects `data` was set with.
|
|
157
|
+
*
|
|
158
|
+
* Built for "paste a list of business keys, select the matching rows" flows — e.g. a user
|
|
159
|
+
* pastes order numbers and the host selects whichever loaded rows match, without the host
|
|
160
|
+
* needing to know how visual/data indices relate.
|
|
161
|
+
*/
|
|
162
|
+
selectWhere(predicate: (row: DataRow, dataIndex: number) => boolean): void;
|
|
141
163
|
private _dispatchRowSelectionEvent;
|
|
142
164
|
/**
|
|
143
165
|
* Set a filter for a column. Replaces any existing filter on the same key.
|
|
@@ -400,6 +422,12 @@ export declare class FlexTable extends LitElement {
|
|
|
400
422
|
render(): TemplateResult<1>;
|
|
401
423
|
private _onSelectAllChange;
|
|
402
424
|
private _onRowCheckboxChange;
|
|
425
|
+
/**
|
|
426
|
+
* Captures shiftKey from the checkbox's own click (MouseEvent) — the subsequent native
|
|
427
|
+
* `change` event does not carry modifier keys. Click always fires before change for a
|
|
428
|
+
* checkbox input, so this reliably primes `_onRowCheckboxChange` for the same interaction.
|
|
429
|
+
*/
|
|
430
|
+
private _onRowCheckboxClick;
|
|
403
431
|
private _renderFrozenRows;
|
|
404
432
|
private _renderFooter;
|
|
405
433
|
private _renderRow;
|
package/dist/flex-table.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, i as t, n, r, t as i } from "./flex-table-
|
|
1
|
+
import { a as e, i as t, n, r, t as i } from "./flex-table-CeT8zkEi.js";
|
|
2
2
|
export { i as FlexTable, t as RowSelectionState, r as UndoStack, n as exportData, e as renderCell };
|
package/dist/models/types.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface ColumnDefinition<T = DataRow> {
|
|
|
59
59
|
hidden?: boolean;
|
|
60
60
|
/** Whether the column is sortable (default: true) */
|
|
61
61
|
sortable?: boolean;
|
|
62
|
+
/**
|
|
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.
|
|
67
|
+
*/
|
|
68
|
+
headerAlign?: 'start' | 'center' | 'end';
|
|
62
69
|
/** Custom cell renderer — overrides built-in type rendering */
|
|
63
70
|
renderer?: CellRenderer<T>;
|
|
64
71
|
/** Whether the column is editable (default: true — follows global editable setting) */
|
package/dist/react.js
CHANGED