@jh-grid/jhgrid-js 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -16,38 +16,18 @@ performance, editing, filtering, and frozen columns, running in your browser rig
16
16
 
17
17
  ## Features
18
18
 
19
- - Smooth rendering via `requestAnimationFrame` + Canvas 2D, synced to your display's native refresh rate (60Hz, 120Hz, 144Hz, etc.), not capped at 60fps
20
- - 2D virtual scrolling: only visible cells are drawn
21
- - Chunk-based async data loading with prefetch & cache
22
- - HiDPI / Retina display support (devicePixelRatio scaling)
23
- - Draggable scrollbars (vertical + horizontal)
24
- - Left/right frozen columns (`frozenCols` / `frozenColsRight`)
25
- - **Cell click**: single cell selection with blue border highlight
26
- - **Cell drag**: multi-cell range selection with fill overlay
27
- - **Row selection**: single/multi row selection (`rowSelection: 'single' | 'multi'`), with an optional select-all header checkbox (`columnDefs[].headerCheckbox`)
28
- - **Row drag reorder**: drag rows by the row-number gutter (`rowReorder: true`)
29
- - **Double-click to edit**: per-column editable/readonly control
30
- - **Ctrl+C / Ctrl+V**: copy & paste (single cell or range, TSV format)
31
- - **Ctrl+Z / Ctrl+Y**: undo / redo (cell edits, row/column add/delete)
32
- - **Column validation**: declarative required/pattern/min/max/length/custom rules with red-border + tooltip error display (`min`/`max` compare chronologically on a `type: 'date'` column)
33
- - **In-cell action buttons**: `type: 'button'` columns render a clickable pill per row (e.g. "Delete", "Approve") independent of `editableCols`
34
- - **Date / rich-text / image cell types**: `type: 'date'` opens a native date/datetime picker; `type: 'richtext'` opens an inline bold/italic/underline/strikethrough editor; `type: 'image'` renders a cell image (`fit: 'cover' | 'contain'`, size-aware decoding, shared LRU cache), plus a pluggable `CellEditors`/`CellRenderers` registry (`registerCellEditor()`/`registerCellRenderer()`) for fully custom editors and renderers
35
- - **Set filter**: checkbox list of a column's distinct values in the header filter panel (`setFilterValues()`)
36
- - **Quick filter**: global cross-column search term (`setQuickFilter()` / `getQuickFilter()` / `clearQuickFilter()`)
37
- - **Single-column sort**: `setSort()` / `removeSort()` / `clearSort()`
38
- - **Row / column CRUD**: `addRow()`/`deleteRow()`/`undeleteRow()`, `addColumn()`/`deleteColumn()`/`undeleteColumn()`, with matching `getNew*()`/`getDeleted*()` accessors for diff-based saves
39
- - **Column hide/show**: `hideColumn()` / `showColumn()` / `isColumnVisible()` / `getHiddenColumns()`, plus per-row/column resize (`setRowHeight()`, `autoFitColumns()`)
40
- - **Multi-level header groups**: `columnDefs[].group` (or explicit `headerRows`) merges header cells across levels
41
- - **Per-row / per-cell styling callbacks**: `rowHighlighter` / `cellBackground` for conditional formatting
42
- - **State snapshot/restore**: `getState()` / `setState()` for saving and restoring grid state (filters, sort, column order/visibility, edits)
43
- - **Built-in localization**: `locale` option with bundled `KO_I18N` / `JA_I18N` / `ZH_I18N` text packs, per-key `i18n` overrides, and locale-aware number/date/currency cell rendering
44
- - **Accessibility**: ARIA labeling, keyboard-navigable header/row focus, and automatic high-contrast (`forced-colors`) theme remapping
45
- - **CSV export + print preview**: `exportCsv()`, `printGrid()`
46
- - **Arrow key navigation**: keyboard-driven cell movement
47
- - **Enter / Tab**: commit edit and move to next row / column
48
- - Text overflow with ellipsis (`…`): O(log n) binary search
49
- - Fully themeable
50
- - Zero dependencies
19
+ - **Canvas rendering + 2D virtualization** smooth at 60/120/144Hz, HiDPI-aware
20
+ - **Large-data loading** chunk-based async loading with prefetch & cache
21
+ - **Frozen columns & scrollbars** — left/right freezing with draggable vertical/horizontal scrollbars
22
+ - **Selection & row operations** — cell/range selection, single/multi row selection, row drag reorder
23
+ - **Editing** inline editing, validation, undo/redo, TSV copy & paste
24
+ - **Rich cell types** dropdown, multiselect, checkbox, date, richtext, image, button, plus custom editors/renderers
25
+ - **Filtering & sorting** set filter, quick filter, single-column sort
26
+ - **CRUD & change tracking** row/column add/delete with diff-based persistence
27
+ - **Headers & styling** multi-level headers, conditional row/cell styling
28
+ - **State & export** state snapshot/restore, CSV export, print preview
29
+ - **i18n & accessibility** KO/JA/ZH localization, ARIA, keyboard navigation, high-contrast support
30
+ - **Zero dependencies & theming** fully themeable with no runtime dependencies
51
31
 
52
32
  ---
53
33
 
@@ -4,6 +4,14 @@ var DataManager = class {
4
4
  #chunkSize;
5
5
  #maxChunks;
6
6
  #cache = /* @__PURE__ */ new Map();
7
+ // 1-entry cache of the chunk getRow() touched last. A bulk row-major walk (e.g. clearing an
8
+ // entire selection) calls getRow() for every column of the same row back-to-back -- same
9
+ // chunkIdx every time -- and #chunkSize more rows after that before it changes, so this turns
10
+ // the overwhelming majority of getRow() calls into a single index comparison instead of a Map
11
+ // lookup plus the LRU delete+re-insert below. Must be invalidated everywhere #cache forgets a
12
+ // chunk (#evict, clear) so it can never serve an entry #cache itself no longer considers current.
13
+ #lastChunkIdx = -1;
14
+ #lastChunk = null;
7
15
  #fetching = /* @__PURE__ */ new Set();
8
16
  #failed = /* @__PURE__ */ new Set();
9
17
  #failedTimers = /* @__PURE__ */ new Set();
@@ -37,10 +45,13 @@ var DataManager = class {
37
45
  // re-request after onChunkLoaded).
38
46
  getRow(rowIndex) {
39
47
  const chunkIdx = Math.floor(rowIndex / this.#chunkSize);
48
+ if (chunkIdx === this.#lastChunkIdx) return this.#lastChunk[rowIndex % this.#chunkSize] ?? null;
40
49
  if (this.#cache.has(chunkIdx)) {
41
50
  const chunk = this.#cache.get(chunkIdx);
42
51
  this.#cache.delete(chunkIdx);
43
52
  this.#cache.set(chunkIdx, chunk);
53
+ this.#lastChunkIdx = chunkIdx;
54
+ this.#lastChunk = chunk;
44
55
  return chunk[rowIndex % this.#chunkSize] ?? null;
45
56
  }
46
57
  this.#request(chunkIdx);
@@ -201,6 +212,10 @@ var DataManager = class {
201
212
  while (this.#cache.size > this.#maxChunks) {
202
213
  const oldest = this.#cache.keys().next().value;
203
214
  this.#cache.delete(oldest);
215
+ if (oldest === this.#lastChunkIdx) {
216
+ this.#lastChunkIdx = -1;
217
+ this.#lastChunk = null;
218
+ }
204
219
  }
205
220
  }
206
221
  // Returning `false` from `callback` stops the walk. Callers that are looking for an answer
@@ -227,6 +242,8 @@ var DataManager = class {
227
242
  this.#failed.clear();
228
243
  this.#failedTimers.forEach(clearTimeout);
229
244
  this.#failedTimers.clear();
245
+ this.#lastChunkIdx = -1;
246
+ this.#lastChunk = null;
230
247
  }
231
248
  };
232
249
 
@@ -1347,7 +1364,9 @@ var Renderer = class _Renderer {
1347
1364
  colPositions,
1348
1365
  hiddenNeighbors,
1349
1366
  headerCheckboxCols,
1350
- sel
1367
+ sel,
1368
+ frozenWidth,
1369
+ vpW
1351
1370
  );
1352
1371
  ctx.restore();
1353
1372
  if (frozenCount > 0) {
@@ -1375,7 +1394,9 @@ var Renderer = class _Renderer {
1375
1394
  colPositions,
1376
1395
  hiddenNeighbors,
1377
1396
  headerCheckboxCols,
1378
- sel
1397
+ sel,
1398
+ 0,
1399
+ frozenWidth
1379
1400
  );
1380
1401
  ctx.restore();
1381
1402
  }
@@ -1404,7 +1425,9 @@ var Renderer = class _Renderer {
1404
1425
  colPositions,
1405
1426
  hiddenNeighbors,
1406
1427
  headerCheckboxCols,
1407
- sel
1428
+ sel,
1429
+ rightX,
1430
+ frozenRightWidth
1408
1431
  );
1409
1432
  ctx.restore();
1410
1433
  }
@@ -1869,7 +1892,7 @@ var Renderer = class _Renderer {
1869
1892
  #colX(c, frozenCount, frozenWidth, frozenRightCount, rightX, scrollLeft, colPositions) {
1870
1893
  return colScreenX(c, { frozenCount, frozenWidth, frozenRightCount, rightX, colPositions }, scrollLeft);
1871
1894
  }
1872
- #drawHeader(labels, aligns, columns, sorts, filters, startCol, endCol, scrollLeft, totalH, rowH, headerRows, theme, frozenCount, frozenWidth, frozenRightCount, rightX, colPositions, hiddenNeighbors, headerCheckboxCols, sel) {
1895
+ #drawHeader(labels, aligns, columns, sorts, filters, startCol, endCol, scrollLeft, totalH, rowH, headerRows, theme, frozenCount, frozenWidth, frozenRightCount, rightX, colPositions, hiddenNeighbors, headerCheckboxCols, sel, clipX = 0, clipW = Infinity) {
1873
1896
  if (startCol > endCol) return;
1874
1897
  const ctx = this.#ctx;
1875
1898
  const padding = theme.cellPadding;
@@ -1943,6 +1966,13 @@ var Renderer = class _Renderer {
1943
1966
  const visEndCol = Math.min(cell.col + cell.colspan - 1, endCol);
1944
1967
  textCx = colPositions[visStartCol] + xOffset;
1945
1968
  textCw = colPositions[visEndCol + 1] - colPositions[visStartCol];
1969
+ const clipRight = clipX + clipW;
1970
+ if (textCx < clipX) {
1971
+ textCw -= clipX - textCx;
1972
+ textCx = clipX;
1973
+ }
1974
+ if (textCx + textCw > clipRight) textCw = clipRight - textCx;
1975
+ textCw = Math.max(0, textCw);
1946
1976
  }
1947
1977
  const align = cell.isLeaf ? aligns?.[cell.col] ?? "left" : cell.align ?? "center";
1948
1978
  const iconRsv = cell.isLeaf ? FILTER_ICON_W + 4 : 0;
@@ -2322,7 +2352,7 @@ var Renderer = class _Renderer {
2322
2352
  }
2323
2353
  ctx.restore();
2324
2354
  }
2325
- #drawScrollbars({ v, h, SB, vSB = SB, hSB = SB, frozenWidth = 0, frozenRightWidth = 0, rightX = 0 }, theme, W, H) {
2355
+ #drawScrollbars({ v, h, SB, vSB = SB, hSB = SB, frozenWidth = 0, frozenRightWidth = 0, rightX = 0, headerH = 0 }, theme, W, H) {
2326
2356
  if (!vSB && !hSB) return;
2327
2357
  const ctx = this.#ctx;
2328
2358
  const r = theme.scrollbarRadius;
@@ -2330,6 +2360,7 @@ var Renderer = class _Renderer {
2330
2360
  if (vSB > 0 && hSB > 0) ctx.fillRect(W - vSB, H - hSB, vSB, hSB);
2331
2361
  if (hSB > 0 && frozenWidth > 0) ctx.fillRect(0, H - hSB, frozenWidth, hSB);
2332
2362
  if (hSB > 0 && frozenRightWidth > 0) ctx.fillRect(rightX, H - hSB, frozenRightWidth, hSB);
2363
+ if (vSB > 0 && headerH > 0) ctx.fillRect(W - vSB, 0, vSB, headerH);
2333
2364
  ctx.fillRect(v.x, v.y, v.w, v.h);
2334
2365
  ctx.fillStyle = theme.scrollbarThumb;
2335
2366
  this.#roundRect(v.x + 2, v.thumbY + 2, v.w - 4, v.thumbH - 4, r);
@@ -4785,7 +4816,11 @@ function buildFilterPanelEl({
4785
4816
  clearTimeout(debounce);
4786
4817
  seq++;
4787
4818
  });
4788
- readTags = () => state.contains !== null ? { contains: state.contains } : { values: [...state.tags] };
4819
+ readTags = () => {
4820
+ const pending = tagInput.value.trim();
4821
+ if (pending && state.contains === null && state.tags.length === 0) setContains(pending);
4822
+ return state.contains !== null ? { contains: state.contains } : { values: [...state.tags] };
4823
+ };
4789
4824
  }
4790
4825
  let valuesSection = null;
4791
4826
  const valueCheckboxes = [];
@@ -4846,11 +4881,13 @@ function buildFilterPanelEl({
4846
4881
  });
4847
4882
  valueCheckboxes.forEach((cb) => cb.addEventListener("change", syncSelectAll));
4848
4883
  valuesSearch.addEventListener("input", () => {
4884
+ const pristine = valueCheckboxes.every((cb) => cb.checked);
4849
4885
  const q = valuesSearch.value.trim().toLowerCase();
4850
4886
  let anyVisible = false;
4851
4887
  for (const r of valueRows) {
4852
4888
  const match = !q || r.text.toLowerCase().includes(q);
4853
4889
  r.row.style.display = match ? "flex" : "none";
4890
+ if (pristine) r.cb.checked = match;
4854
4891
  anyVisible = anyVisible || match;
4855
4892
  }
4856
4893
  noMatch.style.display = anyVisible ? "none" : "block";
@@ -7392,7 +7429,14 @@ var JHGrid = class _JHGrid {
7392
7429
  this._pendingLocalState = null;
7393
7430
  return Promise.resolve();
7394
7431
  }
7395
- this._dm.setFetch((page, size) => this._opts.fetchData(page, size, state));
7432
+ const fetchData = this._isLocalData ? /* @__PURE__ */ (() => {
7433
+ let all = null;
7434
+ return (page, size) => {
7435
+ all ??= this._opts.fetchData(0, Number.MAX_SAFE_INTEGER, state).then((r) => r.rows);
7436
+ return all.then((rows) => ({ rows: rows.slice(page * size, page * size + size) }));
7437
+ };
7438
+ })() : (page, size) => this._opts.fetchData(page, size, state);
7439
+ this._dm.setFetch(fetchData);
7396
7440
  return this._boot(state);
7397
7441
  }
7398
7442
  // Where the sweep sits across the loading bars, 0..1, or null to leave them flat.
@@ -8786,19 +8830,19 @@ var JHGrid = class _JHGrid {
8786
8830
  }
8787
8831
  _clearSelection() {
8788
8832
  if (!this._sel) return;
8789
- const pairs = this._sel.type === "single" ? [[this._sel.row, this._sel.col]] : Array.from(
8790
- { length: this._sel.r2 - this._sel.r1 + 1 },
8791
- (_, ri) => Array.from(
8792
- { length: this._sel.c2 - this._sel.c1 + 1 },
8793
- (_2, ci) => [this._sel.r1 + ri, this._sel.c1 + ci]
8794
- )
8795
- ).flat();
8833
+ const single = this._sel.type === "single";
8834
+ const r1 = single ? this._sel.row : this._sel.r1;
8835
+ const r2 = single ? this._sel.row : this._sel.r2;
8836
+ const c1 = single ? this._sel.col : this._sel.c1;
8837
+ const c2 = single ? this._sel.col : this._sel.c2;
8838
+ const editableFields = [];
8839
+ for (let c = c1; c <= c2; c++) {
8840
+ if (this._isEditable(c)) editableFields.push(this._columns[c]);
8841
+ }
8796
8842
  this._editTxnBegin();
8797
- pairs.forEach(([r, c]) => {
8798
- if (!this._isEditable(c)) return;
8799
- const field = this._columns[c];
8800
- this._setEdit(r, field, "");
8801
- });
8843
+ for (let r = r1; r <= r2; r++) {
8844
+ for (const field of editableFields) this._setEdit(r, field, "");
8845
+ }
8802
8846
  this._editTxnCommit();
8803
8847
  this._draw();
8804
8848
  }
@@ -8983,11 +9027,15 @@ var JHGrid = class _JHGrid {
8983
9027
  const v = data[field];
8984
9028
  return v != null ? String(v) : "";
8985
9029
  }
8986
- // Re-runs validation for one cell and updates the validator's invalid-cell set.
9030
+ // Re-runs validation for one cell and updates the validator's invalid-cell set. Takes a
9031
+ // "row_field" key -- for callers that only have that (iterating an _edits-shaped Map). Callers
9032
+ // that already have row/field apart (_setEdit, validateAll) should call _revalidateRowField()
9033
+ // directly instead of paying to stringify them together here just to split them back apart.
8987
9034
  _revalidateKey(key) {
8988
9035
  const u = key.indexOf("_");
8989
- const row = Number(key.slice(0, u));
8990
- const field = key.slice(u + 1);
9036
+ this._revalidateRowField(Number(key.slice(0, u)), key.slice(u + 1));
9037
+ }
9038
+ _revalidateRowField(row, field) {
8991
9039
  this._validator.revalidate(row, field, this._resolveCellStringValue(row, field));
8992
9040
  }
8993
9041
  // Rebuilds invalid-cell state from scratch based on the current _edits map —
@@ -9022,11 +9070,11 @@ var JHGrid = class _JHGrid {
9022
9070
  const validatedFields = this._columns.filter((f) => this._colDefMap.get(f)?.validation);
9023
9071
  if (validatedFields.length > 0) {
9024
9072
  this._dm.forEachLoaded((_, rowIndex) => {
9025
- validatedFields.forEach((field) => this._revalidateKey(`${rowIndex}_${field}`));
9073
+ validatedFields.forEach((field) => this._revalidateRowField(rowIndex, field));
9026
9074
  });
9027
9075
  this._localRows.forEach((_, i) => {
9028
9076
  const r = this._rowPlan.visualOfLocal(i);
9029
- validatedFields.forEach((field) => this._revalidateKey(`${r}_${field}`));
9077
+ validatedFields.forEach((field) => this._revalidateRowField(r, field));
9030
9078
  });
9031
9079
  }
9032
9080
  this._draw();
@@ -9053,7 +9101,7 @@ var JHGrid = class _JHGrid {
9053
9101
  this._edits.set(key, val);
9054
9102
  this._editedRows.add(row);
9055
9103
  this._opts.onCellChange?.({ row, field, newValue: val, oldValue });
9056
- this._revalidateKey(key);
9104
+ this._revalidateRowField(row, field);
9057
9105
  this._growRowForMultilineValue(row, val);
9058
9106
  }
9059
9107
  // Excel grows a row's height the moment a cell picks up a line break -- typed (Alt+Enter) or
@@ -11917,7 +11965,7 @@ var JHGrid = class _JHGrid {
11917
11965
  JHGrid.use(RowSelectionPlugin);
11918
11966
 
11919
11967
  // index.js
11920
- var VERSION = "0.1.1";
11968
+ var VERSION = "0.1.3";
11921
11969
  var SUPPORTED_BROWSERS = {
11922
11970
  chrome: 99,
11923
11971
  edge: 99,
package/dist/jhgrid.js CHANGED
@@ -44,6 +44,14 @@ var JHGrid = (() => {
44
44
  #chunkSize;
45
45
  #maxChunks;
46
46
  #cache = /* @__PURE__ */ new Map();
47
+ // 1-entry cache of the chunk getRow() touched last. A bulk row-major walk (e.g. clearing an
48
+ // entire selection) calls getRow() for every column of the same row back-to-back -- same
49
+ // chunkIdx every time -- and #chunkSize more rows after that before it changes, so this turns
50
+ // the overwhelming majority of getRow() calls into a single index comparison instead of a Map
51
+ // lookup plus the LRU delete+re-insert below. Must be invalidated everywhere #cache forgets a
52
+ // chunk (#evict, clear) so it can never serve an entry #cache itself no longer considers current.
53
+ #lastChunkIdx = -1;
54
+ #lastChunk = null;
47
55
  #fetching = /* @__PURE__ */ new Set();
48
56
  #failed = /* @__PURE__ */ new Set();
49
57
  #failedTimers = /* @__PURE__ */ new Set();
@@ -77,10 +85,13 @@ var JHGrid = (() => {
77
85
  // re-request after onChunkLoaded).
78
86
  getRow(rowIndex) {
79
87
  const chunkIdx = Math.floor(rowIndex / this.#chunkSize);
88
+ if (chunkIdx === this.#lastChunkIdx) return this.#lastChunk[rowIndex % this.#chunkSize] ?? null;
80
89
  if (this.#cache.has(chunkIdx)) {
81
90
  const chunk = this.#cache.get(chunkIdx);
82
91
  this.#cache.delete(chunkIdx);
83
92
  this.#cache.set(chunkIdx, chunk);
93
+ this.#lastChunkIdx = chunkIdx;
94
+ this.#lastChunk = chunk;
84
95
  return chunk[rowIndex % this.#chunkSize] ?? null;
85
96
  }
86
97
  this.#request(chunkIdx);
@@ -241,6 +252,10 @@ var JHGrid = (() => {
241
252
  while (this.#cache.size > this.#maxChunks) {
242
253
  const oldest = this.#cache.keys().next().value;
243
254
  this.#cache.delete(oldest);
255
+ if (oldest === this.#lastChunkIdx) {
256
+ this.#lastChunkIdx = -1;
257
+ this.#lastChunk = null;
258
+ }
244
259
  }
245
260
  }
246
261
  // Returning `false` from `callback` stops the walk. Callers that are looking for an answer
@@ -267,6 +282,8 @@ var JHGrid = (() => {
267
282
  this.#failed.clear();
268
283
  this.#failedTimers.forEach(clearTimeout);
269
284
  this.#failedTimers.clear();
285
+ this.#lastChunkIdx = -1;
286
+ this.#lastChunk = null;
270
287
  }
271
288
  };
272
289
 
@@ -1387,7 +1404,9 @@ var JHGrid = (() => {
1387
1404
  colPositions,
1388
1405
  hiddenNeighbors,
1389
1406
  headerCheckboxCols,
1390
- sel
1407
+ sel,
1408
+ frozenWidth,
1409
+ vpW
1391
1410
  );
1392
1411
  ctx.restore();
1393
1412
  if (frozenCount > 0) {
@@ -1415,7 +1434,9 @@ var JHGrid = (() => {
1415
1434
  colPositions,
1416
1435
  hiddenNeighbors,
1417
1436
  headerCheckboxCols,
1418
- sel
1437
+ sel,
1438
+ 0,
1439
+ frozenWidth
1419
1440
  );
1420
1441
  ctx.restore();
1421
1442
  }
@@ -1444,7 +1465,9 @@ var JHGrid = (() => {
1444
1465
  colPositions,
1445
1466
  hiddenNeighbors,
1446
1467
  headerCheckboxCols,
1447
- sel
1468
+ sel,
1469
+ rightX,
1470
+ frozenRightWidth
1448
1471
  );
1449
1472
  ctx.restore();
1450
1473
  }
@@ -1909,7 +1932,7 @@ var JHGrid = (() => {
1909
1932
  #colX(c, frozenCount, frozenWidth, frozenRightCount, rightX, scrollLeft, colPositions) {
1910
1933
  return colScreenX(c, { frozenCount, frozenWidth, frozenRightCount, rightX, colPositions }, scrollLeft);
1911
1934
  }
1912
- #drawHeader(labels, aligns, columns, sorts, filters, startCol, endCol, scrollLeft, totalH, rowH, headerRows, theme, frozenCount, frozenWidth, frozenRightCount, rightX, colPositions, hiddenNeighbors, headerCheckboxCols, sel) {
1935
+ #drawHeader(labels, aligns, columns, sorts, filters, startCol, endCol, scrollLeft, totalH, rowH, headerRows, theme, frozenCount, frozenWidth, frozenRightCount, rightX, colPositions, hiddenNeighbors, headerCheckboxCols, sel, clipX = 0, clipW = Infinity) {
1913
1936
  if (startCol > endCol) return;
1914
1937
  const ctx = this.#ctx;
1915
1938
  const padding = theme.cellPadding;
@@ -1983,6 +2006,13 @@ var JHGrid = (() => {
1983
2006
  const visEndCol = Math.min(cell.col + cell.colspan - 1, endCol);
1984
2007
  textCx = colPositions[visStartCol] + xOffset;
1985
2008
  textCw = colPositions[visEndCol + 1] - colPositions[visStartCol];
2009
+ const clipRight = clipX + clipW;
2010
+ if (textCx < clipX) {
2011
+ textCw -= clipX - textCx;
2012
+ textCx = clipX;
2013
+ }
2014
+ if (textCx + textCw > clipRight) textCw = clipRight - textCx;
2015
+ textCw = Math.max(0, textCw);
1986
2016
  }
1987
2017
  const align = cell.isLeaf ? aligns?.[cell.col] ?? "left" : cell.align ?? "center";
1988
2018
  const iconRsv = cell.isLeaf ? FILTER_ICON_W + 4 : 0;
@@ -2362,7 +2392,7 @@ var JHGrid = (() => {
2362
2392
  }
2363
2393
  ctx.restore();
2364
2394
  }
2365
- #drawScrollbars({ v, h, SB, vSB = SB, hSB = SB, frozenWidth = 0, frozenRightWidth = 0, rightX = 0 }, theme, W, H) {
2395
+ #drawScrollbars({ v, h, SB, vSB = SB, hSB = SB, frozenWidth = 0, frozenRightWidth = 0, rightX = 0, headerH = 0 }, theme, W, H) {
2366
2396
  if (!vSB && !hSB) return;
2367
2397
  const ctx = this.#ctx;
2368
2398
  const r = theme.scrollbarRadius;
@@ -2370,6 +2400,7 @@ var JHGrid = (() => {
2370
2400
  if (vSB > 0 && hSB > 0) ctx.fillRect(W - vSB, H - hSB, vSB, hSB);
2371
2401
  if (hSB > 0 && frozenWidth > 0) ctx.fillRect(0, H - hSB, frozenWidth, hSB);
2372
2402
  if (hSB > 0 && frozenRightWidth > 0) ctx.fillRect(rightX, H - hSB, frozenRightWidth, hSB);
2403
+ if (vSB > 0 && headerH > 0) ctx.fillRect(W - vSB, 0, vSB, headerH);
2373
2404
  ctx.fillRect(v.x, v.y, v.w, v.h);
2374
2405
  ctx.fillStyle = theme.scrollbarThumb;
2375
2406
  this.#roundRect(v.x + 2, v.thumbY + 2, v.w - 4, v.thumbH - 4, r);
@@ -4825,7 +4856,11 @@ var JHGrid = (() => {
4825
4856
  clearTimeout(debounce);
4826
4857
  seq++;
4827
4858
  });
4828
- readTags = () => state.contains !== null ? { contains: state.contains } : { values: [...state.tags] };
4859
+ readTags = () => {
4860
+ const pending = tagInput.value.trim();
4861
+ if (pending && state.contains === null && state.tags.length === 0) setContains(pending);
4862
+ return state.contains !== null ? { contains: state.contains } : { values: [...state.tags] };
4863
+ };
4829
4864
  }
4830
4865
  let valuesSection = null;
4831
4866
  const valueCheckboxes = [];
@@ -4886,11 +4921,13 @@ var JHGrid = (() => {
4886
4921
  });
4887
4922
  valueCheckboxes.forEach((cb) => cb.addEventListener("change", syncSelectAll));
4888
4923
  valuesSearch.addEventListener("input", () => {
4924
+ const pristine = valueCheckboxes.every((cb) => cb.checked);
4889
4925
  const q = valuesSearch.value.trim().toLowerCase();
4890
4926
  let anyVisible = false;
4891
4927
  for (const r of valueRows) {
4892
4928
  const match = !q || r.text.toLowerCase().includes(q);
4893
4929
  r.row.style.display = match ? "flex" : "none";
4930
+ if (pristine) r.cb.checked = match;
4894
4931
  anyVisible = anyVisible || match;
4895
4932
  }
4896
4933
  noMatch.style.display = anyVisible ? "none" : "block";
@@ -7432,7 +7469,14 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
7432
7469
  this._pendingLocalState = null;
7433
7470
  return Promise.resolve();
7434
7471
  }
7435
- this._dm.setFetch((page, size) => this._opts.fetchData(page, size, state));
7472
+ const fetchData = this._isLocalData ? /* @__PURE__ */ (() => {
7473
+ let all = null;
7474
+ return (page, size) => {
7475
+ all ??= this._opts.fetchData(0, Number.MAX_SAFE_INTEGER, state).then((r) => r.rows);
7476
+ return all.then((rows) => ({ rows: rows.slice(page * size, page * size + size) }));
7477
+ };
7478
+ })() : (page, size) => this._opts.fetchData(page, size, state);
7479
+ this._dm.setFetch(fetchData);
7436
7480
  return this._boot(state);
7437
7481
  }
7438
7482
  // Where the sweep sits across the loading bars, 0..1, or null to leave them flat.
@@ -8826,19 +8870,19 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
8826
8870
  }
8827
8871
  _clearSelection() {
8828
8872
  if (!this._sel) return;
8829
- const pairs = this._sel.type === "single" ? [[this._sel.row, this._sel.col]] : Array.from(
8830
- { length: this._sel.r2 - this._sel.r1 + 1 },
8831
- (_, ri) => Array.from(
8832
- { length: this._sel.c2 - this._sel.c1 + 1 },
8833
- (_2, ci) => [this._sel.r1 + ri, this._sel.c1 + ci]
8834
- )
8835
- ).flat();
8873
+ const single = this._sel.type === "single";
8874
+ const r1 = single ? this._sel.row : this._sel.r1;
8875
+ const r2 = single ? this._sel.row : this._sel.r2;
8876
+ const c1 = single ? this._sel.col : this._sel.c1;
8877
+ const c2 = single ? this._sel.col : this._sel.c2;
8878
+ const editableFields = [];
8879
+ for (let c = c1; c <= c2; c++) {
8880
+ if (this._isEditable(c)) editableFields.push(this._columns[c]);
8881
+ }
8836
8882
  this._editTxnBegin();
8837
- pairs.forEach(([r, c]) => {
8838
- if (!this._isEditable(c)) return;
8839
- const field = this._columns[c];
8840
- this._setEdit(r, field, "");
8841
- });
8883
+ for (let r = r1; r <= r2; r++) {
8884
+ for (const field of editableFields) this._setEdit(r, field, "");
8885
+ }
8842
8886
  this._editTxnCommit();
8843
8887
  this._draw();
8844
8888
  }
@@ -9023,11 +9067,15 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9023
9067
  const v = data[field];
9024
9068
  return v != null ? String(v) : "";
9025
9069
  }
9026
- // Re-runs validation for one cell and updates the validator's invalid-cell set.
9070
+ // Re-runs validation for one cell and updates the validator's invalid-cell set. Takes a
9071
+ // "row_field" key -- for callers that only have that (iterating an _edits-shaped Map). Callers
9072
+ // that already have row/field apart (_setEdit, validateAll) should call _revalidateRowField()
9073
+ // directly instead of paying to stringify them together here just to split them back apart.
9027
9074
  _revalidateKey(key) {
9028
9075
  const u = key.indexOf("_");
9029
- const row = Number(key.slice(0, u));
9030
- const field = key.slice(u + 1);
9076
+ this._revalidateRowField(Number(key.slice(0, u)), key.slice(u + 1));
9077
+ }
9078
+ _revalidateRowField(row, field) {
9031
9079
  this._validator.revalidate(row, field, this._resolveCellStringValue(row, field));
9032
9080
  }
9033
9081
  // Rebuilds invalid-cell state from scratch based on the current _edits map —
@@ -9062,11 +9110,11 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9062
9110
  const validatedFields = this._columns.filter((f) => this._colDefMap.get(f)?.validation);
9063
9111
  if (validatedFields.length > 0) {
9064
9112
  this._dm.forEachLoaded((_, rowIndex) => {
9065
- validatedFields.forEach((field) => this._revalidateKey(`${rowIndex}_${field}`));
9113
+ validatedFields.forEach((field) => this._revalidateRowField(rowIndex, field));
9066
9114
  });
9067
9115
  this._localRows.forEach((_, i) => {
9068
9116
  const r = this._rowPlan.visualOfLocal(i);
9069
- validatedFields.forEach((field) => this._revalidateKey(`${r}_${field}`));
9117
+ validatedFields.forEach((field) => this._revalidateRowField(r, field));
9070
9118
  });
9071
9119
  }
9072
9120
  this._draw();
@@ -9093,7 +9141,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9093
9141
  this._edits.set(key, val);
9094
9142
  this._editedRows.add(row);
9095
9143
  this._opts.onCellChange?.({ row, field, newValue: val, oldValue });
9096
- this._revalidateKey(key);
9144
+ this._revalidateRowField(row, field);
9097
9145
  this._growRowForMultilineValue(row, val);
9098
9146
  }
9099
9147
  // Excel grows a row's height the moment a cell picks up a line break -- typed (Alt+Enter) or
@@ -11957,7 +12005,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
11957
12005
  JHGrid.use(RowSelectionPlugin);
11958
12006
 
11959
12007
  // index.js
11960
- var VERSION = "0.1.1";
12008
+ var VERSION = "0.1.3";
11961
12009
  var SUPPORTED_BROWSERS = {
11962
12010
  chrome: 99,
11963
12011
  edge: 99,