@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 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.143.0/helpers/esm/decorate.js
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 ? "No data" : "No matching data";
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({
@@ -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;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-DZFtW02v.js";
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 };
@@ -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
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-DZFtW02v.js";
1
+ import { t as e } from "./flex-table-CeT8zkEi.js";
2
2
  import t from "react";
3
3
  import { createComponent as n } from "@lit/react";
4
4
  var r = n({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",