@iyulab/flex-table 0.26.0 → 0.28.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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.0] - 2026-08-23
4
+
5
+ ### Added
6
+
7
+ - **`stylesheets: CSSStyleSheet[]`** — constructable stylesheets adopted into the grid's shadow
8
+ root alongside its own styles. A `ColumnDefinition.renderer` returns content that's inserted
9
+ inside `<flex-table>`'s shadow root, so a class-based utility from the host document's CSS
10
+ (`.is-xs`, a design-system size variant, anything selector-based rather than a CSS custom
11
+ property) never reaches it — the class attaches but no rule matches, since document stylesheets
12
+ don't cross the shadow boundary. Passing the same `CSSStyleSheet` the host document already
13
+ uses (`document.adoptedStyleSheets`) into this new property closes that gap without copying
14
+ CSS or reaching into internals. Reassigning `stylesheets` swaps the previously-adopted sheets
15
+ for the new ones — it doesn't accumulate — and the grid's own styles are never affected either
16
+ way.
17
+
18
+ ## [0.27.0] - 2026-08-23
19
+
20
+ ### Added
21
+
22
+ - **Shift-click range selection on the row checkbox column** (`selectable` + `selectionMode:
23
+ 'multi'`). Clicking a row's checkbox while holding Shift now selects every row between it and
24
+ the last row you clicked, matching the row-selection convention used by spreadsheets and file
25
+ managers. A plain click still just toggles the one row it lands on, and the anchor resets to
26
+ whichever row you last clicked (shift or not) — the same rules as native cell selection already
27
+ followed elsewhere in the grid.
28
+ - **`selectWhere(predicate)`** — selects every currently-loaded row for which `predicate(row,
29
+ dataIndex)` returns true, added to whatever is already selected (call `deselectAll()` first for
30
+ a fresh set). Built for "the user pastes a list of keys, select the matching rows" flows: the
31
+ host doesn't need to reason about visual-vs-data row indices or virtualization — it gets the
32
+ same row objects `data` was set with.
33
+ - **`emptyMessage` / `noMatchingMessage`** — the text shown in the empty state (`data` is empty,
34
+ or every row is hidden by an active column filter) was previously hardcoded to `'No data'` /
35
+ `'No matching data'` with no way to override it. Any consumer localizing their UI, or wanting a
36
+ more specific message ("no orders yet — create one"), had to reimplement the empty state
37
+ themselves. Both now default to the same English strings for backward compatibility, and can be
38
+ overridden as string properties/attributes (`empty-message`, `no-matching-message`).
39
+
3
40
  ## [0.26.0] - 2026-08-20
4
41
 
5
42
  ### Added
package/README.md CHANGED
@@ -84,6 +84,9 @@ npm install @iyulab/flex-table
84
84
  | `selectionMode` | `selection-mode` | `'single' \| 'multi'` | `'multi'` | Row selection mode |
85
85
  | `dataMode` | `data-mode` | `'client' \| 'server'` | `'client'` | Client-side or server-side data processing |
86
86
  | `footerData` | `footer-data` | `Record<string, string>` | `null` | Footer/summary row data (keys match column keys) |
87
+ | `emptyMessage` | `empty-message` | `string` | `'No data'` | Shown when `data` is empty |
88
+ | `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | Shown when `data` has rows but every one is hidden by an active column filter |
89
+ | `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Constructable stylesheets adopted into the shadow root alongside the grid's own styles — the escape hatch for styling content a `renderer` inserts, since document CSS doesn't cross the shadow boundary. Reassigning swaps the previous set, it doesn't accumulate |
87
90
 
88
91
  ### Read-only Properties
89
92
 
@@ -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.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], 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", {
@@ -2098,7 +2115,10 @@ var J = {
2098
2115
  super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown), this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
2099
2116
  }
2100
2117
  firstUpdated() {
2101
- this._readDensityTokens(), this._measureViewport();
2118
+ this._readDensityTokens(), this._measureViewport(), this.shadowRoot && Array.isArray(this.shadowRoot.adoptedStyleSheets) && (this._baseStyleSheets = [...this.shadowRoot.adoptedStyleSheets]), this._syncStylesheets();
2119
+ }
2120
+ _syncStylesheets() {
2121
+ !this.shadowRoot || !Array.isArray(this.shadowRoot.adoptedStyleSheets) || (this.shadowRoot.adoptedStyleSheets = [...this._baseStyleSheets, ...this.stylesheets]);
2102
2122
  }
2103
2123
  _readDensityTokens() {
2104
2124
  let e = getComputedStyle(this).getPropertyValue("--ft-row-height").trim();
@@ -2144,7 +2164,7 @@ var J = {
2144
2164
  (!(e.size <= 2 && !e.has("data") && !e.has("columns") && !e.has("_openFilterKey") && !e.has("_editingCell") && !e.has("_activeCell") && (e.has("_scrollTop") || e.has("_scrollLeft"))) || this._viewDirty) && (this._recomputeView(), this._viewDirty = !1), this._updateColOffsets(), this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length), this._rowSelection.setRowCount(this._visibleRowCount);
2145
2165
  }
2146
2166
  updated(e) {
2147
- this._focusEditor(), this._adjustFilterDropdown();
2167
+ this._focusEditor(), this._adjustFilterDropdown(), e.has("stylesheets") && this._syncStylesheets();
2148
2168
  let t = String(this._visibleRowCount), n = String(this.visibleColumns.length);
2149
2169
  if (this.getAttribute("aria-rowcount") !== t && this.setAttribute("aria-rowcount", t), this.getAttribute("aria-colcount") !== n && this.setAttribute("aria-colcount", n), this.loading ? this.getAttribute("aria-busy") !== "true" && this.setAttribute("aria-busy", "true") : this.hasAttribute("aria-busy") && this.removeAttribute("aria-busy"), e.has("_commentPopup") && this._commentPopup) {
2150
2170
  let e = this.shadowRoot?.querySelector(".ft-comment-popup textarea");
@@ -3794,7 +3814,7 @@ var J = {
3794
3814
  for (let t = s; t < c; t++) m.push(this._renderHeaderCell(e[t], t));
3795
3815
  let h = this._colDragIndicatorLeft == null ? "" : n`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
3796
3816
  if (this.data.length === 0 || this._visibleRowCount === 0) {
3797
- let e = this.data.length === 0 ? "No data" : "No matching data";
3817
+ let e = this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage;
3798
3818
  return n`
3799
3819
  ${i}
3800
3820
  <div class="ft-header" role="row" style="width: ${o}px; height: ${a}px;">
@@ -3833,7 +3853,10 @@ var J = {
3833
3853
  e.target.checked ? this._rowSelection.selectAll() : this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
3834
3854
  }
3835
3855
  _onRowCheckboxChange(e, t) {
3836
- e.stopPropagation(), this._rowSelection.toggle(t), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
3856
+ 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();
3857
+ }
3858
+ _onRowCheckboxClick(e) {
3859
+ this._checkboxShiftPending = e.shiftKey;
3837
3860
  }
3838
3861
  _renderFrozenRows(e, t, i) {
3839
3862
  let a = this._frozenRowCount;
@@ -3880,6 +3903,7 @@ var J = {
3880
3903
  style="position: absolute; top: 0; left: ${m + h}px; width: 36px; height: ${u}px; z-index: 2;">
3881
3904
  <input type="checkbox"
3882
3905
  .checked=${p}
3906
+ @click=${(e) => this._onRowCheckboxClick(e)}
3883
3907
  @change=${(t) => this._onRowCheckboxChange(t, e)}>
3884
3908
  </div>
3885
3909
  ` : "";
@@ -4055,6 +4079,12 @@ q([a({ type: Array })], $.prototype, "columns", void 0), q([a({
4055
4079
  type: Number,
4056
4080
  attribute: "frozen-rows"
4057
4081
  })], $.prototype, "frozenRows", void 0), q([a({
4082
+ type: String,
4083
+ attribute: "empty-message"
4084
+ })], $.prototype, "emptyMessage", void 0), q([a({
4085
+ type: String,
4086
+ attribute: "no-matching-message"
4087
+ })], $.prototype, "noMatchingMessage", void 0), q([a({
4058
4088
  type: Boolean,
4059
4089
  reflect: !0
4060
4090
  })], $.prototype, "loading", void 0), q([a({
@@ -4069,7 +4099,7 @@ q([a({ type: Array })], $.prototype, "columns", void 0), q([a({
4069
4099
  })], $.prototype, "dataMode", void 0), q([a({
4070
4100
  type: Object,
4071
4101
  attribute: "footer-data"
4072
- })], $.prototype, "footerData", void 0), q([a({
4102
+ })], $.prototype, "footerData", void 0), q([a({ attribute: !1 })], $.prototype, "stylesheets", void 0), q([a({
4073
4103
  type: Number,
4074
4104
  attribute: "max-undo-size"
4075
4105
  })], $.prototype, "maxUndoSize", null), q([o()], $.prototype, "_scrollTop", void 0), q([o()], $.prototype, "_scrollLeft", void 0), q([o()], $.prototype, "_viewportHeight", void 0), q([o()], $.prototype, "_viewportWidth", void 0), q([o()], $.prototype, "_activeCell", void 0), q([o()], $.prototype, "_editingCell", void 0), q([o()], $.prototype, "_sortCriteria", void 0), q([o()], $.prototype, "_openFilterKey", void 0), q([o()], $.prototype, "_rowSelectionVersion", void 0), q([o()], $.prototype, "_autocompleteState", void 0), q([o()], $.prototype, "_headerMenu", void 0), q([o()], $.prototype, "_bodyContextMenu", void 0), q([o()], $.prototype, "_commentPopup", void 0), q([o()], $.prototype, "_isDragOver", void 0), $ = q([i("flex-table")], $);
@@ -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`과 자연 연동하도록 설계됨:
@@ -78,6 +85,14 @@ export declare class FlexTable extends LitElement {
78
85
  dataMode: DataMode;
79
86
  /** Footer/summary row data. Keys match column keys; values are display strings. */
80
87
  footerData: Record<string, string | TemplateResult> | null;
88
+ /**
89
+ * Constructable stylesheets adopted into this element's shadow root, in addition to the
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`
92
+ * returns — this is the escape hatch for that. Not an attribute (a `CSSStyleSheet` can't be
93
+ * serialized to one) — set it as a property.
94
+ */
95
+ stylesheets: CSSStyleSheet[];
81
96
  set maxUndoSize(value: number);
82
97
  get maxUndoSize(): number;
83
98
  private _scrollTop;
@@ -86,6 +101,8 @@ export declare class FlexTable extends LitElement {
86
101
  private _viewportWidth;
87
102
  private _colLeftOffsets;
88
103
  private _totalRowWidth;
104
+ /** The grid's own compiled styles, captured once so `stylesheets` merges without dropping them. */
105
+ private _baseStyleSheets;
89
106
  private _activeCell;
90
107
  private _editingCell;
91
108
  private _sortCriteria;
@@ -98,6 +115,10 @@ export declare class FlexTable extends LitElement {
98
115
  private _sortedIndices;
99
116
  private _openFilterKey;
100
117
  private _rowSelectionVersion;
118
+ /** Anchor row for shift-click range selection on the row checkbox column. */
119
+ private _lastCheckboxRowIndex;
120
+ /** Set by the checkbox's own click (which carries shiftKey) just before its change event fires. */
121
+ private _checkboxShiftPending;
101
122
  private _autocompleteState;
102
123
  private _headerMenu;
103
124
  private _bodyContextMenu;
@@ -138,6 +159,17 @@ export declare class FlexTable extends LitElement {
138
159
  selectAll(): void;
139
160
  /** Deselect all rows. */
140
161
  deselectAll(): void;
162
+ /**
163
+ * Selects every currently-loaded row for which `predicate` returns true, in addition to
164
+ * whatever is already selected — call `deselectAll()` first for a fresh set. Iterates the
165
+ * visible (post filter/sort) view and resolves each visual row to its underlying data row,
166
+ * so the predicate always sees the same objects `data` was set with.
167
+ *
168
+ * Built for "paste a list of business keys, select the matching rows" flows — e.g. a user
169
+ * pastes order numbers and the host selects whichever loaded rows match, without the host
170
+ * needing to know how visual/data indices relate.
171
+ */
172
+ selectWhere(predicate: (row: DataRow, dataIndex: number) => boolean): void;
141
173
  private _dispatchRowSelectionEvent;
142
174
  /**
143
175
  * Set a filter for a column. Replaces any existing filter on the same key.
@@ -272,6 +304,7 @@ export declare class FlexTable extends LitElement {
272
304
  connectedCallback(): void;
273
305
  disconnectedCallback(): void;
274
306
  protected firstUpdated(): void;
307
+ private _syncStylesheets;
275
308
  /**
276
309
  * `--ft-row-height` 를 판독해 가상화 계산에 넣는다.
277
310
  *
@@ -400,6 +433,12 @@ export declare class FlexTable extends LitElement {
400
433
  render(): TemplateResult<1>;
401
434
  private _onSelectAllChange;
402
435
  private _onRowCheckboxChange;
436
+ /**
437
+ * Captures shiftKey from the checkbox's own click (MouseEvent) — the subsequent native
438
+ * `change` event does not carry modifier keys. Click always fires before change for a
439
+ * checkbox input, so this reliably primes `_onRowCheckboxChange` for the same interaction.
440
+ */
441
+ private _onRowCheckboxClick;
403
442
  private _renderFrozenRows;
404
443
  private _renderFooter;
405
444
  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-BQvkNkCu.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-BQvkNkCu.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.28.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",