@jh-grid/jhgrid-js 0.1.0 → 0.1.1

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
@@ -3,6 +3,9 @@
3
3
  High-performance Canvas-based data grid with smooth 2D virtualization.
4
4
  Renders millions of rows and columns with near-zero DOM overhead.
5
5
 
6
+ **[▶ Try the live demo](https://jh-grid.github.io/JHGrid/docs/demo)**: 1,000,000-row scroll
7
+ performance, editing, filtering, and frozen columns, running in your browser right now.
8
+
6
9
  ![JH Grid screenshot](docs/images/jhgrid.png)
7
10
 
8
11
  > This repository distributes the **pre-built bundle** (`jhgrid.esm.js` / `jhgrid.js` /
@@ -50,9 +53,22 @@ Renders millions of rows and columns with near-zero DOM overhead.
50
53
 
51
54
  ## Installation
52
55
 
53
- > **npm package coming soon.** For now, use one of the two options below.
56
+ ### Option A: npm (recommended for bundled apps)
57
+
58
+ ```bash
59
+ npm install @jh-grid/jhgrid-js
60
+ ```
61
+
62
+ ```js
63
+ import { JHGrid } from '@jh-grid/jhgrid-js';
64
+ ```
65
+
66
+ The package ships as **ES Modules only** and includes its own TypeScript declarations
67
+ (`index.d.ts`), so no `@types/` package is needed. Any bundler (Vite, webpack, Rollup, esbuild) and
68
+ modern Node ESM can consume it directly. CommonJS `require('@jh-grid/jhgrid-js')` is *not* supported; use a
69
+ dynamic `await import('@jh-grid/jhgrid-js')` if you must load it from a CJS file.
54
70
 
55
- ### Option A: Static ES module (no bundler)
71
+ ### Option B: Static ES module (no bundler)
56
72
 
57
73
  `jhgrid.esm.js` is a single self-contained ES module file: deploy it as-is as a static resource
58
74
  (e.g. from a Spring Boot static resource path) and import it directly, no build step required:
@@ -63,7 +79,7 @@ Renders millions of rows and columns with near-zero DOM overhead.
63
79
  </script>
64
80
  ```
65
81
 
66
- ### Option B: CDN (single bundled script)
82
+ ### Option C: CDN (single bundled script)
67
83
 
68
84
  `jhgrid.min.js` is an IIFE build served straight from this repository via jsDelivr, no npm
69
85
  install required. Everything is exposed on a single global, `JHGrid` (the grid constructor is
@@ -93,7 +109,7 @@ no fetch functions needed:
93
109
  <div id="my-grid"></div>
94
110
 
95
111
  <script type="module">
96
- import { JHGrid } from './dist/jhgrid.esm.js'; // adjust to wherever you host the file
112
+ import { JHGrid } from '@jh-grid/jhgrid-js';
97
113
 
98
114
  const grid = new JHGrid({
99
115
  container: '#my-grid',
@@ -129,7 +145,7 @@ instead:
129
145
  <div id="my-grid"></div>
130
146
 
131
147
  <script type="module">
132
- import { JHGrid } from './dist/jhgrid.esm.js'; // adjust to wherever you host the file
148
+ import { JHGrid } from '@jh-grid/jhgrid-js';
133
149
 
134
150
  const grid = new JHGrid({
135
151
  container: '#my-grid',
@@ -4314,6 +4314,12 @@ function buildFilterPanelEl({
4314
4314
  multiSortEnabled,
4315
4315
  i18n,
4316
4316
  theme,
4317
+ // wrapperHeight + openUpward: when the caller found more room above the header than below (a
4318
+ // grid scrolled to sit low on the page — see _openFilterPanel), the panel anchors its *bottom*
4319
+ // to the header's top edge and grows upward instead of down, same as a native <select> flipping
4320
+ // when it would otherwise overflow the viewport.
4321
+ wrapperHeight,
4322
+ openUpward,
4317
4323
  // Set filter (checkbox list of exact values) — distinctValues is null when the column has too
4318
4324
  // many/no loaded values yet, in which case the panel falls back to the plain text search box
4319
4325
  // below instead of rendering a checklist. selectedValues is the currently-applied array filter
@@ -4338,7 +4344,6 @@ function buildFilterPanelEl({
4338
4344
  }) {
4339
4345
  const PANEL_W = 220;
4340
4346
  const panelX = Math.max(0, Math.min(colLeft, width - PANEL_W));
4341
- const panelY = headerH;
4342
4347
  const s = (...parts) => parts.join(";");
4343
4348
  const btn = (text, action, style) => {
4344
4349
  const b = document.createElement("button");
@@ -4357,7 +4362,10 @@ function buildFilterPanelEl({
4357
4362
  el.style.cssText = s(
4358
4363
  `position:absolute`,
4359
4364
  `left:${panelX}px`,
4360
- `top:${panelY}px`,
4365
+ // Anchoring the bottom edge (instead of top) lets the panel grow upward from the header
4366
+ // without knowing its own height up front — the alternative, computing `top: headerH -
4367
+ // panelHeight`, needs the rendered height before it's laid out.
4368
+ openUpward ? `bottom:${Math.max(0, wrapperHeight - headerH)}px` : `top:${headerH}px`,
4361
4369
  `width:${PANEL_W}px`,
4362
4370
  `background:${themed(theme, "overlayBg")}`,
4363
4371
  `border:1px solid ${themed(theme, "overlayBorder")}`,
@@ -5923,6 +5931,8 @@ function resolveIntlTag(locale) {
5923
5931
  // JHGrid.js
5924
5932
  var ARROWS = { ArrowDown: [1, 0], ArrowUp: [-1, 0], ArrowRight: [0, 1], ArrowLeft: [0, -1] };
5925
5933
  var RESIZE_HIT_W = 5;
5934
+ var RESIZE_HIT_W_TOUCH = 8;
5935
+ var CANVAS_TOUCH_CSS = "touch-action:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;";
5926
5936
  var MIN_COL_W = 30;
5927
5937
  var MIN_ROW_H = 16;
5928
5938
  var LONG_PRESS_MS = 500;
@@ -6344,7 +6354,7 @@ var JHGrid = class _JHGrid {
6344
6354
  this._canvas = document.createElement("canvas");
6345
6355
  this._canvas.width = Math.round(width * dpr);
6346
6356
  this._canvas.height = Math.round(height * dpr);
6347
- this._canvas.style.cssText = `display:block;width:${width}px;height:${height}px;outline:none;`;
6357
+ this._canvas.style.cssText = `display:block;width:${width}px;height:${height}px;outline:none;${CANVAS_TOUCH_CSS}`;
6348
6358
  this._canvas.setAttribute("aria-hidden", "true");
6349
6359
  this._canvas.tabIndex = -1;
6350
6360
  this._canvas.getContext("2d").scale(dpr, dpr);
@@ -6574,7 +6584,7 @@ var JHGrid = class _JHGrid {
6574
6584
  this._opts.height = gridHeight;
6575
6585
  this._wrapper.style.width = width + "px";
6576
6586
  this._wrapper.style.height = gridHeight + "px";
6577
- this._canvas.style.cssText = `display:block;width:${width}px;height:${gridHeight}px;outline:none;`;
6587
+ this._canvas.style.cssText = `display:block;width:${width}px;height:${gridHeight}px;outline:none;${CANVAS_TOUCH_CSS}`;
6578
6588
  const dpr = window.devicePixelRatio || 1;
6579
6589
  this._canvas.width = Math.round(width * dpr);
6580
6590
  this._canvas.height = Math.round(gridHeight * dpr);
@@ -6999,29 +7009,85 @@ var JHGrid = class _JHGrid {
6999
7009
  // of, or null. Checks both the hovered row's bottom edge and (falling back, like the column
7000
7010
  // check does with col > 0) the previous row's bottom edge when y lands right at a row's top
7001
7011
  // edge — both branches identify the same boundary line, resolving to "the row above it".
7002
- _hitRowBoundary(y, geo = this._geo()) {
7012
+ _hitRowBoundary(y, geo = this._geo(), hitW = RESIZE_HIT_W) {
7003
7013
  const { height: H } = this._opts;
7004
7014
  if (y < geo.headerH || y >= H - geo.hSB) return null;
7005
7015
  const relY = y - geo.headerH + this._scrollTop;
7006
7016
  const row = this._rowLayout.rowAt(relY, this._totalRows - 1);
7007
7017
  if (row < 0) return null;
7008
7018
  const bottomY = this._rowLayout.yOf(row + 1) - this._scrollTop + geo.headerH;
7009
- if (Math.abs(y - bottomY) <= RESIZE_HIT_W) return row;
7019
+ if (Math.abs(y - bottomY) <= hitW) return row;
7010
7020
  if (row > 0) {
7011
7021
  const topY = this._rowLayout.yOf(row) - this._scrollTop + geo.headerH;
7012
- if (Math.abs(y - topY) <= RESIZE_HIT_W) return row - 1;
7022
+ if (Math.abs(y - topY) <= hitW) return row - 1;
7013
7023
  }
7014
7024
  return null;
7015
7025
  }
7016
- _hitFillHandle(x, y, geo = this._geo()) {
7026
+ // Vertical/horizontal scrollbar hit test, shared by mousedown and touchstart: grabbing the
7027
+ // thumb arms this._drag (consumed by _updateScrollbarDrag on the matching move handler),
7028
+ // tapping the bare track jump-scrolls straight to that position, same as a native scrollbar.
7029
+ // Returns whether (x, y) was inside either scrollbar's hit area at all, so the caller knows to
7030
+ // stop dispatching (preventDefault + return) instead of falling through to header/cell/pan
7031
+ // handling — without this, a touch on the drawn scrollbar was indistinguishable from a touch
7032
+ // anywhere else on the canvas and just started a generic content-drag pan.
7033
+ _hitScrollbar(x, y, geo) {
7034
+ const { v, h } = geo;
7035
+ if (x >= v.x) {
7036
+ if (y >= v.thumbY && y <= v.thumbY + v.thumbH) {
7037
+ this._drag = { axis: "v", startMouse: y, startScroll: this._scrollTop };
7038
+ } else if (y >= v.y && y <= v.y + v.h) {
7039
+ const ratio = Math.max(0, Math.min(1, (y - v.y - v.thumbH / 2) / (v.h - v.thumbH)));
7040
+ this._scrollTop = geo.minScrollY + ratio * (geo.maxScrollY - geo.minScrollY);
7041
+ this._clamp(geo);
7042
+ this._draw();
7043
+ }
7044
+ return true;
7045
+ }
7046
+ if (y >= h.y) {
7047
+ if (x >= h.thumbX && x <= h.thumbX + h.thumbW) {
7048
+ this._drag = { axis: "h", startMouse: x, startScroll: this._scrollLeft };
7049
+ } else if (x >= h.x && x <= h.x + h.w) {
7050
+ const ratio = Math.max(0, Math.min(1, (x - h.x - h.thumbW / 2) / (h.w - h.thumbW)));
7051
+ this._scrollLeft = ratio * geo.maxScrollX;
7052
+ this._clamp(geo);
7053
+ this._draw();
7054
+ }
7055
+ return true;
7056
+ }
7057
+ return false;
7058
+ }
7059
+ // Applies an in-progress scrollbar thumb drag (this._drag, armed by _hitScrollbar) given the
7060
+ // pointer/touch's current raw canvas coordinates. Shared by mousemove and touchmove so the two
7061
+ // input paths can't drift apart.
7062
+ _updateScrollbarDrag(x, y) {
7063
+ const geo = this._geo();
7064
+ const { v, h } = geo;
7065
+ if (this._drag.axis === "v") {
7066
+ const ratio = (y - this._drag.startMouse) / (v.h - v.thumbH);
7067
+ this._scrollTop = this._drag.startScroll + ratio * (geo.maxScrollY - geo.minScrollY);
7068
+ } else {
7069
+ const ratio = (x - this._drag.startMouse) / (h.w - h.thumbW);
7070
+ this._scrollLeft = this._drag.startScroll + ratio * geo.maxScrollX;
7071
+ }
7072
+ this._clamp(geo);
7073
+ this._scrolling = true;
7074
+ this._dm.setHold(true);
7075
+ clearTimeout(this._scrollEndTimer);
7076
+ this._scrollEndTimer = setTimeout(() => {
7077
+ this._scrolling = false;
7078
+ this._dm.setHold(false);
7079
+ this._schedDraw();
7080
+ }, 150);
7081
+ this._schedDraw();
7082
+ }
7083
+ _hitFillHandle(x, y, geo = this._geo(), hitW = RESIZE_HIT_W) {
7017
7084
  if (!this._sel) return false;
7018
7085
  const { headerH, colPositions } = geo;
7019
7086
  const selR2 = this._sel.type === "single" ? this._sel.row : this._sel.r2;
7020
7087
  const selC2 = this._sel.type === "single" ? this._sel.col : this._sel.c2;
7021
7088
  const handleX = this._colLeft(selC2, geo) + (colPositions[selC2 + 1] - colPositions[selC2]);
7022
7089
  const handleY = this._rowLayout.yOf(selR2 + 1) - this._scrollTop + headerH;
7023
- const HIT = 5;
7024
- return Math.abs(x - handleX) <= HIT && Math.abs(y - handleY) <= HIT;
7090
+ return Math.abs(x - handleX) <= hitW && Math.abs(y - handleY) <= hitW;
7025
7091
  }
7026
7092
  _colLeft(col, geo) {
7027
7093
  return colScreenX(col, geo, this._scrollLeft);
@@ -7579,10 +7645,16 @@ var JHGrid = class _JHGrid {
7579
7645
  this._wrapper.style.overflow = "visible";
7580
7646
  const wrapperTop = this._wrapper.getBoundingClientRect().top;
7581
7647
  const viewportH = window.innerHeight || document.documentElement.clientHeight;
7582
- const maxPanelHeight = viewportH - wrapperTop - geo.headerH - 8;
7648
+ const spaceBelow = viewportH - wrapperTop - geo.headerH - 8;
7649
+ const spaceAbove = wrapperTop - 8;
7650
+ const PANEL_MIN_USABLE = 260;
7651
+ const openUpward = spaceBelow < PANEL_MIN_USABLE && spaceAbove > spaceBelow;
7652
+ const maxPanelHeight = openUpward ? spaceAbove : spaceBelow;
7583
7653
  const el = buildFilterPanelEl({
7584
7654
  colLeft: this._colLeft(col, geo),
7585
7655
  headerH: geo.headerH,
7656
+ wrapperHeight: this._opts.height,
7657
+ openUpward,
7586
7658
  width: this._opts.width,
7587
7659
  maxPanelHeight,
7588
7660
  label,
@@ -9636,7 +9708,6 @@ var JHGrid = class _JHGrid {
9636
9708
  ev.mousedown = (e) => {
9637
9709
  const { x, y } = this._xy(e);
9638
9710
  const geo = this._geo();
9639
- const { v, h } = geo;
9640
9711
  const suppressReopenCell = this._suppressReopenCell;
9641
9712
  this._suppressReopenCell = null;
9642
9713
  if (this._editing) this._commitEdit();
@@ -9652,27 +9723,7 @@ var JHGrid = class _JHGrid {
9652
9723
  if (withinSel) return;
9653
9724
  }
9654
9725
  }
9655
- if (x >= v.x) {
9656
- if (y >= v.thumbY && y <= v.thumbY + v.thumbH) {
9657
- this._drag = { axis: "v", startMouse: y, startScroll: this._scrollTop };
9658
- } else if (y >= v.y && y <= v.y + v.h) {
9659
- const ratio = Math.max(0, Math.min(1, (y - v.y - v.thumbH / 2) / (v.h - v.thumbH)));
9660
- this._scrollTop = geo.minScrollY + ratio * (geo.maxScrollY - geo.minScrollY);
9661
- this._clamp(geo);
9662
- this._draw();
9663
- }
9664
- e.preventDefault();
9665
- return;
9666
- }
9667
- if (y >= h.y) {
9668
- if (x >= h.thumbX && x <= h.thumbX + h.thumbW) {
9669
- this._drag = { axis: "h", startMouse: x, startScroll: this._scrollLeft };
9670
- } else if (x >= h.x && x <= h.x + h.w) {
9671
- const ratio = Math.max(0, Math.min(1, (x - h.x - h.thumbW / 2) / (h.w - h.thumbW)));
9672
- this._scrollLeft = ratio * geo.maxScrollX;
9673
- this._clamp(geo);
9674
- this._draw();
9675
- }
9726
+ if (this._hitScrollbar(x, y, geo)) {
9676
9727
  e.preventDefault();
9677
9728
  return;
9678
9729
  }
@@ -9884,25 +9935,7 @@ var JHGrid = class _JHGrid {
9884
9935
  }
9885
9936
  }
9886
9937
  if (this._drag) {
9887
- const geo = this._geo();
9888
- const { v, h } = geo;
9889
- if (this._drag.axis === "v") {
9890
- const ratio = (y - this._drag.startMouse) / (v.h - v.thumbH);
9891
- this._scrollTop = this._drag.startScroll + ratio * (geo.maxScrollY - geo.minScrollY);
9892
- } else {
9893
- const ratio = (x - this._drag.startMouse) / (h.w - h.thumbW);
9894
- this._scrollLeft = this._drag.startScroll + ratio * geo.maxScrollX;
9895
- }
9896
- this._clamp(geo);
9897
- this._scrolling = true;
9898
- this._dm.setHold(true);
9899
- clearTimeout(this._scrollEndTimer);
9900
- this._scrollEndTimer = setTimeout(() => {
9901
- this._scrolling = false;
9902
- this._dm.setHold(false);
9903
- this._schedDraw();
9904
- }, 150);
9905
- this._schedDraw();
9938
+ this._updateScrollbarDrag(x, y);
9906
9939
  return;
9907
9940
  }
9908
9941
  if (this._selDragging) {
@@ -10289,19 +10322,23 @@ var JHGrid = class _JHGrid {
10289
10322
  this._rowTap = null;
10290
10323
  const geo = this._geo();
10291
10324
  const headerH = geo.headerH;
10325
+ if (this._hitScrollbar(x, y, geo)) {
10326
+ e.preventDefault();
10327
+ return;
10328
+ }
10292
10329
  if (y >= 0 && y < headerH) {
10293
10330
  const col = this._hitHeader(x, geo);
10294
10331
  if (col !== null) {
10295
10332
  const colW = geo.colPositions[col + 1] - geo.colPositions[col];
10296
10333
  const colRight = this._colLeft(col, geo) + colW;
10297
10334
  const filterIconX = colRight - FILTER_ICON_W;
10298
- if (Math.abs(x - colRight) <= RESIZE_HIT_W) {
10335
+ if (Math.abs(x - colRight) <= RESIZE_HIT_W_TOUCH) {
10299
10336
  this._colResize = { col, startX: x, startWidth: colW, undoBefore: this._snapshotStructural() };
10300
10337
  e.preventDefault();
10301
10338
  return;
10302
10339
  }
10303
10340
  const colLeft = this._colLeft(col, geo);
10304
- if (col > 0 && Math.abs(x - colLeft) <= RESIZE_HIT_W) {
10341
+ if (col > 0 && Math.abs(x - colLeft) <= RESIZE_HIT_W_TOUCH) {
10305
10342
  const prevColW = geo.colPositions[col] - geo.colPositions[col - 1];
10306
10343
  this._colResize = { col: col - 1, startX: x, startWidth: prevColW, undoBefore: this._snapshotStructural() };
10307
10344
  e.preventDefault();
@@ -10313,7 +10350,7 @@ var JHGrid = class _JHGrid {
10313
10350
  const _hcbCx = this._colLeft(col, geo) + _hcbPad + 6;
10314
10351
  const _hcbCell = computeHeaderCells(this._opts.headerRows, this._columns, this._opts.columnLetterHeader).find((c) => c.isLeaf && c.col === col);
10315
10352
  const _hcbCy = _hcbCell ? _hcbCell.row * this._opts.headerHeight + _hcbCell.rowspan * this._opts.headerHeight / 2 : headerH - this._opts.headerHeight / 2;
10316
- if (Math.abs(x - _hcbCx) <= 9 && Math.abs(y - _hcbCy) <= 9) {
10353
+ if (Math.abs(x - _hcbCx) <= RESIZE_HIT_W_TOUCH && Math.abs(y - _hcbCy) <= RESIZE_HIT_W_TOUCH) {
10317
10354
  const _hcbChecked = !(this._headerCheckboxState.get(_hcbField) ?? false);
10318
10355
  this._headerCheckboxState.set(_hcbField, _hcbChecked);
10319
10356
  this._draw();
@@ -10343,7 +10380,7 @@ var JHGrid = class _JHGrid {
10343
10380
  return;
10344
10381
  }
10345
10382
  }
10346
- const boundaryRow = geo.rowNumW > 0 && x < geo.rowNumW && y >= geo.headerH ? this._hitRowBoundary(y, geo) : null;
10383
+ const boundaryRow = geo.rowNumW > 0 && x < geo.rowNumW && y >= geo.headerH ? this._hitRowBoundary(y, geo, RESIZE_HIT_W_TOUCH) : null;
10347
10384
  if (boundaryRow !== null) {
10348
10385
  this._rowResize = { row: boundaryRow, startY: y, startHeight: this._rowLayout.heightOf(boundaryRow), undoBefore: this._snapshotStructural() };
10349
10386
  e.preventDefault();
@@ -10365,7 +10402,7 @@ var JHGrid = class _JHGrid {
10365
10402
  }
10366
10403
  return;
10367
10404
  }
10368
- if (this._sel && this._hitFillHandle(x, y, geo)) {
10405
+ if (this._sel && this._hitFillHandle(x, y, geo, RESIZE_HIT_W_TOUCH)) {
10369
10406
  this._fillDrag = { sel: this._sel };
10370
10407
  this._fillPreview = null;
10371
10408
  e.preventDefault();
@@ -10403,6 +10440,14 @@ var JHGrid = class _JHGrid {
10403
10440
  e.preventDefault();
10404
10441
  return;
10405
10442
  }
10443
+ if (this._drag) {
10444
+ if (!e.touches.length) return;
10445
+ const t = e.touches[0];
10446
+ const rect2 = this._canvas.getBoundingClientRect();
10447
+ this._updateScrollbarDrag(t.clientX - rect2.left, t.clientY - rect2.top);
10448
+ e.preventDefault();
10449
+ return;
10450
+ }
10406
10451
  if (!this._touch) return;
10407
10452
  const rect = this._canvas.getBoundingClientRect();
10408
10453
  if (e.touches.length >= 2 && this._touch.twoFinger) {
@@ -10439,6 +10484,10 @@ var JHGrid = class _JHGrid {
10439
10484
  ev.touchend = (e) => {
10440
10485
  this._cancelLongPress();
10441
10486
  if (this._finalizeStructuralGesture()) return;
10487
+ if (this._drag) {
10488
+ this._drag = null;
10489
+ return;
10490
+ }
10442
10491
  if (this._rowTap) {
10443
10492
  const { row, ctrlKey, shiftKey } = this._rowTap;
10444
10493
  this._rowTap = null;
@@ -11868,7 +11917,7 @@ var JHGrid = class _JHGrid {
11868
11917
  JHGrid.use(RowSelectionPlugin);
11869
11918
 
11870
11919
  // index.js
11871
- var VERSION = "0.1.0";
11920
+ var VERSION = "0.1.1";
11872
11921
  var SUPPORTED_BROWSERS = {
11873
11922
  chrome: 99,
11874
11923
  edge: 99,