@iyulab/flex-table 0.26.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,27 @@
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
+
3
25
  ## [0.26.0] - 2026-08-20
4
26
 
5
27
  ### Added
@@ -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
  }
@@ -1113,6 +1113,14 @@ var y = class {
1113
1113
  deselectAll() {
1114
1114
  this._selected.clear();
1115
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
+ }
1116
1124
  };
1117
1125
  //#endregion
1118
1126
  //#region src/core/sorting.ts
@@ -1556,7 +1564,7 @@ var J = {
1556
1564
  lte: "≤"
1557
1565
  }, Y = 120, X = 40, Z = 32, Q = 5, $ = class extends e {
1558
1566
  constructor(...e) {
1559
- 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 = () => {
1560
1568
  this._commitCommentPopup();
1561
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();
1562
1570
  }
@@ -1627,6 +1635,15 @@ var J = {
1627
1635
  deselectAll() {
1628
1636
  this.selectable && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent());
1629
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
+ }
1630
1647
  _dispatchRowSelectionEvent() {
1631
1648
  let { selectedIndices: e, selectedRows: t } = this.getSelectedRows();
1632
1649
  this.dispatchEvent(new CustomEvent("selection-change", {
@@ -3794,7 +3811,7 @@ var J = {
3794
3811
  for (let t = s; t < c; t++) m.push(this._renderHeaderCell(e[t], t));
3795
3812
  let h = this._colDragIndicatorLeft == null ? "" : n`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
3796
3813
  if (this.data.length === 0 || this._visibleRowCount === 0) {
3797
- let e = this.data.length === 0 ? "No data" : "No matching data";
3814
+ let e = this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage;
3798
3815
  return n`
3799
3816
  ${i}
3800
3817
  <div class="ft-header" role="row" style="width: ${o}px; height: ${a}px;">
@@ -3833,7 +3850,10 @@ var J = {
3833
3850
  e.target.checked ? this._rowSelection.selectAll() : this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
3834
3851
  }
3835
3852
  _onRowCheckboxChange(e, t) {
3836
- 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;
3837
3857
  }
3838
3858
  _renderFrozenRows(e, t, i) {
3839
3859
  let a = this._frozenRowCount;
@@ -3880,6 +3900,7 @@ var J = {
3880
3900
  style="position: absolute; top: 0; left: ${m + h}px; width: 36px; height: ${u}px; z-index: 2;">
3881
3901
  <input type="checkbox"
3882
3902
  .checked=${p}
3903
+ @click=${(e) => this._onRowCheckboxClick(e)}
3883
3904
  @change=${(t) => this._onRowCheckboxChange(t, e)}>
3884
3905
  </div>
3885
3906
  ` : "";
@@ -4055,6 +4076,12 @@ q([a({ type: Array })], $.prototype, "columns", void 0), q([a({
4055
4076
  type: Number,
4056
4077
  attribute: "frozen-rows"
4057
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({
4058
4085
  type: Boolean,
4059
4086
  reflect: !0
4060
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-BSxnKOL3.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 };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-BSxnKOL3.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.26.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",