@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/dist/jhgrid.js CHANGED
@@ -4354,6 +4354,12 @@ var JHGrid = (() => {
4354
4354
  multiSortEnabled,
4355
4355
  i18n,
4356
4356
  theme,
4357
+ // wrapperHeight + openUpward: when the caller found more room above the header than below (a
4358
+ // grid scrolled to sit low on the page — see _openFilterPanel), the panel anchors its *bottom*
4359
+ // to the header's top edge and grows upward instead of down, same as a native <select> flipping
4360
+ // when it would otherwise overflow the viewport.
4361
+ wrapperHeight,
4362
+ openUpward,
4357
4363
  // Set filter (checkbox list of exact values) — distinctValues is null when the column has too
4358
4364
  // many/no loaded values yet, in which case the panel falls back to the plain text search box
4359
4365
  // below instead of rendering a checklist. selectedValues is the currently-applied array filter
@@ -4378,7 +4384,6 @@ var JHGrid = (() => {
4378
4384
  }) {
4379
4385
  const PANEL_W = 220;
4380
4386
  const panelX = Math.max(0, Math.min(colLeft, width - PANEL_W));
4381
- const panelY = headerH;
4382
4387
  const s = (...parts) => parts.join(";");
4383
4388
  const btn = (text, action, style) => {
4384
4389
  const b = document.createElement("button");
@@ -4397,7 +4402,10 @@ var JHGrid = (() => {
4397
4402
  el.style.cssText = s(
4398
4403
  `position:absolute`,
4399
4404
  `left:${panelX}px`,
4400
- `top:${panelY}px`,
4405
+ // Anchoring the bottom edge (instead of top) lets the panel grow upward from the header
4406
+ // without knowing its own height up front — the alternative, computing `top: headerH -
4407
+ // panelHeight`, needs the rendered height before it's laid out.
4408
+ openUpward ? `bottom:${Math.max(0, wrapperHeight - headerH)}px` : `top:${headerH}px`,
4401
4409
  `width:${PANEL_W}px`,
4402
4410
  `background:${themed(theme, "overlayBg")}`,
4403
4411
  `border:1px solid ${themed(theme, "overlayBorder")}`,
@@ -5963,6 +5971,8 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
5963
5971
  // JHGrid.js
5964
5972
  var ARROWS = { ArrowDown: [1, 0], ArrowUp: [-1, 0], ArrowRight: [0, 1], ArrowLeft: [0, -1] };
5965
5973
  var RESIZE_HIT_W = 5;
5974
+ var RESIZE_HIT_W_TOUCH = 8;
5975
+ var CANVAS_TOUCH_CSS = "touch-action:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;";
5966
5976
  var MIN_COL_W = 30;
5967
5977
  var MIN_ROW_H = 16;
5968
5978
  var LONG_PRESS_MS = 500;
@@ -6384,7 +6394,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
6384
6394
  this._canvas = document.createElement("canvas");
6385
6395
  this._canvas.width = Math.round(width * dpr);
6386
6396
  this._canvas.height = Math.round(height * dpr);
6387
- this._canvas.style.cssText = `display:block;width:${width}px;height:${height}px;outline:none;`;
6397
+ this._canvas.style.cssText = `display:block;width:${width}px;height:${height}px;outline:none;${CANVAS_TOUCH_CSS}`;
6388
6398
  this._canvas.setAttribute("aria-hidden", "true");
6389
6399
  this._canvas.tabIndex = -1;
6390
6400
  this._canvas.getContext("2d").scale(dpr, dpr);
@@ -6614,7 +6624,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
6614
6624
  this._opts.height = gridHeight;
6615
6625
  this._wrapper.style.width = width + "px";
6616
6626
  this._wrapper.style.height = gridHeight + "px";
6617
- this._canvas.style.cssText = `display:block;width:${width}px;height:${gridHeight}px;outline:none;`;
6627
+ this._canvas.style.cssText = `display:block;width:${width}px;height:${gridHeight}px;outline:none;${CANVAS_TOUCH_CSS}`;
6618
6628
  const dpr = window.devicePixelRatio || 1;
6619
6629
  this._canvas.width = Math.round(width * dpr);
6620
6630
  this._canvas.height = Math.round(gridHeight * dpr);
@@ -7039,29 +7049,85 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
7039
7049
  // of, or null. Checks both the hovered row's bottom edge and (falling back, like the column
7040
7050
  // check does with col > 0) the previous row's bottom edge when y lands right at a row's top
7041
7051
  // edge — both branches identify the same boundary line, resolving to "the row above it".
7042
- _hitRowBoundary(y, geo = this._geo()) {
7052
+ _hitRowBoundary(y, geo = this._geo(), hitW = RESIZE_HIT_W) {
7043
7053
  const { height: H } = this._opts;
7044
7054
  if (y < geo.headerH || y >= H - geo.hSB) return null;
7045
7055
  const relY = y - geo.headerH + this._scrollTop;
7046
7056
  const row = this._rowLayout.rowAt(relY, this._totalRows - 1);
7047
7057
  if (row < 0) return null;
7048
7058
  const bottomY = this._rowLayout.yOf(row + 1) - this._scrollTop + geo.headerH;
7049
- if (Math.abs(y - bottomY) <= RESIZE_HIT_W) return row;
7059
+ if (Math.abs(y - bottomY) <= hitW) return row;
7050
7060
  if (row > 0) {
7051
7061
  const topY = this._rowLayout.yOf(row) - this._scrollTop + geo.headerH;
7052
- if (Math.abs(y - topY) <= RESIZE_HIT_W) return row - 1;
7062
+ if (Math.abs(y - topY) <= hitW) return row - 1;
7053
7063
  }
7054
7064
  return null;
7055
7065
  }
7056
- _hitFillHandle(x, y, geo = this._geo()) {
7066
+ // Vertical/horizontal scrollbar hit test, shared by mousedown and touchstart: grabbing the
7067
+ // thumb arms this._drag (consumed by _updateScrollbarDrag on the matching move handler),
7068
+ // tapping the bare track jump-scrolls straight to that position, same as a native scrollbar.
7069
+ // Returns whether (x, y) was inside either scrollbar's hit area at all, so the caller knows to
7070
+ // stop dispatching (preventDefault + return) instead of falling through to header/cell/pan
7071
+ // handling — without this, a touch on the drawn scrollbar was indistinguishable from a touch
7072
+ // anywhere else on the canvas and just started a generic content-drag pan.
7073
+ _hitScrollbar(x, y, geo) {
7074
+ const { v, h } = geo;
7075
+ if (x >= v.x) {
7076
+ if (y >= v.thumbY && y <= v.thumbY + v.thumbH) {
7077
+ this._drag = { axis: "v", startMouse: y, startScroll: this._scrollTop };
7078
+ } else if (y >= v.y && y <= v.y + v.h) {
7079
+ const ratio = Math.max(0, Math.min(1, (y - v.y - v.thumbH / 2) / (v.h - v.thumbH)));
7080
+ this._scrollTop = geo.minScrollY + ratio * (geo.maxScrollY - geo.minScrollY);
7081
+ this._clamp(geo);
7082
+ this._draw();
7083
+ }
7084
+ return true;
7085
+ }
7086
+ if (y >= h.y) {
7087
+ if (x >= h.thumbX && x <= h.thumbX + h.thumbW) {
7088
+ this._drag = { axis: "h", startMouse: x, startScroll: this._scrollLeft };
7089
+ } else if (x >= h.x && x <= h.x + h.w) {
7090
+ const ratio = Math.max(0, Math.min(1, (x - h.x - h.thumbW / 2) / (h.w - h.thumbW)));
7091
+ this._scrollLeft = ratio * geo.maxScrollX;
7092
+ this._clamp(geo);
7093
+ this._draw();
7094
+ }
7095
+ return true;
7096
+ }
7097
+ return false;
7098
+ }
7099
+ // Applies an in-progress scrollbar thumb drag (this._drag, armed by _hitScrollbar) given the
7100
+ // pointer/touch's current raw canvas coordinates. Shared by mousemove and touchmove so the two
7101
+ // input paths can't drift apart.
7102
+ _updateScrollbarDrag(x, y) {
7103
+ const geo = this._geo();
7104
+ const { v, h } = geo;
7105
+ if (this._drag.axis === "v") {
7106
+ const ratio = (y - this._drag.startMouse) / (v.h - v.thumbH);
7107
+ this._scrollTop = this._drag.startScroll + ratio * (geo.maxScrollY - geo.minScrollY);
7108
+ } else {
7109
+ const ratio = (x - this._drag.startMouse) / (h.w - h.thumbW);
7110
+ this._scrollLeft = this._drag.startScroll + ratio * geo.maxScrollX;
7111
+ }
7112
+ this._clamp(geo);
7113
+ this._scrolling = true;
7114
+ this._dm.setHold(true);
7115
+ clearTimeout(this._scrollEndTimer);
7116
+ this._scrollEndTimer = setTimeout(() => {
7117
+ this._scrolling = false;
7118
+ this._dm.setHold(false);
7119
+ this._schedDraw();
7120
+ }, 150);
7121
+ this._schedDraw();
7122
+ }
7123
+ _hitFillHandle(x, y, geo = this._geo(), hitW = RESIZE_HIT_W) {
7057
7124
  if (!this._sel) return false;
7058
7125
  const { headerH, colPositions } = geo;
7059
7126
  const selR2 = this._sel.type === "single" ? this._sel.row : this._sel.r2;
7060
7127
  const selC2 = this._sel.type === "single" ? this._sel.col : this._sel.c2;
7061
7128
  const handleX = this._colLeft(selC2, geo) + (colPositions[selC2 + 1] - colPositions[selC2]);
7062
7129
  const handleY = this._rowLayout.yOf(selR2 + 1) - this._scrollTop + headerH;
7063
- const HIT = 5;
7064
- return Math.abs(x - handleX) <= HIT && Math.abs(y - handleY) <= HIT;
7130
+ return Math.abs(x - handleX) <= hitW && Math.abs(y - handleY) <= hitW;
7065
7131
  }
7066
7132
  _colLeft(col, geo) {
7067
7133
  return colScreenX(col, geo, this._scrollLeft);
@@ -7619,10 +7685,16 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
7619
7685
  this._wrapper.style.overflow = "visible";
7620
7686
  const wrapperTop = this._wrapper.getBoundingClientRect().top;
7621
7687
  const viewportH = window.innerHeight || document.documentElement.clientHeight;
7622
- const maxPanelHeight = viewportH - wrapperTop - geo.headerH - 8;
7688
+ const spaceBelow = viewportH - wrapperTop - geo.headerH - 8;
7689
+ const spaceAbove = wrapperTop - 8;
7690
+ const PANEL_MIN_USABLE = 260;
7691
+ const openUpward = spaceBelow < PANEL_MIN_USABLE && spaceAbove > spaceBelow;
7692
+ const maxPanelHeight = openUpward ? spaceAbove : spaceBelow;
7623
7693
  const el = buildFilterPanelEl({
7624
7694
  colLeft: this._colLeft(col, geo),
7625
7695
  headerH: geo.headerH,
7696
+ wrapperHeight: this._opts.height,
7697
+ openUpward,
7626
7698
  width: this._opts.width,
7627
7699
  maxPanelHeight,
7628
7700
  label,
@@ -9676,7 +9748,6 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9676
9748
  ev.mousedown = (e) => {
9677
9749
  const { x, y } = this._xy(e);
9678
9750
  const geo = this._geo();
9679
- const { v, h } = geo;
9680
9751
  const suppressReopenCell = this._suppressReopenCell;
9681
9752
  this._suppressReopenCell = null;
9682
9753
  if (this._editing) this._commitEdit();
@@ -9692,27 +9763,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9692
9763
  if (withinSel) return;
9693
9764
  }
9694
9765
  }
9695
- if (x >= v.x) {
9696
- if (y >= v.thumbY && y <= v.thumbY + v.thumbH) {
9697
- this._drag = { axis: "v", startMouse: y, startScroll: this._scrollTop };
9698
- } else if (y >= v.y && y <= v.y + v.h) {
9699
- const ratio = Math.max(0, Math.min(1, (y - v.y - v.thumbH / 2) / (v.h - v.thumbH)));
9700
- this._scrollTop = geo.minScrollY + ratio * (geo.maxScrollY - geo.minScrollY);
9701
- this._clamp(geo);
9702
- this._draw();
9703
- }
9704
- e.preventDefault();
9705
- return;
9706
- }
9707
- if (y >= h.y) {
9708
- if (x >= h.thumbX && x <= h.thumbX + h.thumbW) {
9709
- this._drag = { axis: "h", startMouse: x, startScroll: this._scrollLeft };
9710
- } else if (x >= h.x && x <= h.x + h.w) {
9711
- const ratio = Math.max(0, Math.min(1, (x - h.x - h.thumbW / 2) / (h.w - h.thumbW)));
9712
- this._scrollLeft = ratio * geo.maxScrollX;
9713
- this._clamp(geo);
9714
- this._draw();
9715
- }
9766
+ if (this._hitScrollbar(x, y, geo)) {
9716
9767
  e.preventDefault();
9717
9768
  return;
9718
9769
  }
@@ -9924,25 +9975,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
9924
9975
  }
9925
9976
  }
9926
9977
  if (this._drag) {
9927
- const geo = this._geo();
9928
- const { v, h } = geo;
9929
- if (this._drag.axis === "v") {
9930
- const ratio = (y - this._drag.startMouse) / (v.h - v.thumbH);
9931
- this._scrollTop = this._drag.startScroll + ratio * (geo.maxScrollY - geo.minScrollY);
9932
- } else {
9933
- const ratio = (x - this._drag.startMouse) / (h.w - h.thumbW);
9934
- this._scrollLeft = this._drag.startScroll + ratio * geo.maxScrollX;
9935
- }
9936
- this._clamp(geo);
9937
- this._scrolling = true;
9938
- this._dm.setHold(true);
9939
- clearTimeout(this._scrollEndTimer);
9940
- this._scrollEndTimer = setTimeout(() => {
9941
- this._scrolling = false;
9942
- this._dm.setHold(false);
9943
- this._schedDraw();
9944
- }, 150);
9945
- this._schedDraw();
9978
+ this._updateScrollbarDrag(x, y);
9946
9979
  return;
9947
9980
  }
9948
9981
  if (this._selDragging) {
@@ -10329,19 +10362,23 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10329
10362
  this._rowTap = null;
10330
10363
  const geo = this._geo();
10331
10364
  const headerH = geo.headerH;
10365
+ if (this._hitScrollbar(x, y, geo)) {
10366
+ e.preventDefault();
10367
+ return;
10368
+ }
10332
10369
  if (y >= 0 && y < headerH) {
10333
10370
  const col = this._hitHeader(x, geo);
10334
10371
  if (col !== null) {
10335
10372
  const colW = geo.colPositions[col + 1] - geo.colPositions[col];
10336
10373
  const colRight = this._colLeft(col, geo) + colW;
10337
10374
  const filterIconX = colRight - FILTER_ICON_W;
10338
- if (Math.abs(x - colRight) <= RESIZE_HIT_W) {
10375
+ if (Math.abs(x - colRight) <= RESIZE_HIT_W_TOUCH) {
10339
10376
  this._colResize = { col, startX: x, startWidth: colW, undoBefore: this._snapshotStructural() };
10340
10377
  e.preventDefault();
10341
10378
  return;
10342
10379
  }
10343
10380
  const colLeft = this._colLeft(col, geo);
10344
- if (col > 0 && Math.abs(x - colLeft) <= RESIZE_HIT_W) {
10381
+ if (col > 0 && Math.abs(x - colLeft) <= RESIZE_HIT_W_TOUCH) {
10345
10382
  const prevColW = geo.colPositions[col] - geo.colPositions[col - 1];
10346
10383
  this._colResize = { col: col - 1, startX: x, startWidth: prevColW, undoBefore: this._snapshotStructural() };
10347
10384
  e.preventDefault();
@@ -10353,7 +10390,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10353
10390
  const _hcbCx = this._colLeft(col, geo) + _hcbPad + 6;
10354
10391
  const _hcbCell = computeHeaderCells(this._opts.headerRows, this._columns, this._opts.columnLetterHeader).find((c) => c.isLeaf && c.col === col);
10355
10392
  const _hcbCy = _hcbCell ? _hcbCell.row * this._opts.headerHeight + _hcbCell.rowspan * this._opts.headerHeight / 2 : headerH - this._opts.headerHeight / 2;
10356
- if (Math.abs(x - _hcbCx) <= 9 && Math.abs(y - _hcbCy) <= 9) {
10393
+ if (Math.abs(x - _hcbCx) <= RESIZE_HIT_W_TOUCH && Math.abs(y - _hcbCy) <= RESIZE_HIT_W_TOUCH) {
10357
10394
  const _hcbChecked = !(this._headerCheckboxState.get(_hcbField) ?? false);
10358
10395
  this._headerCheckboxState.set(_hcbField, _hcbChecked);
10359
10396
  this._draw();
@@ -10383,7 +10420,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10383
10420
  return;
10384
10421
  }
10385
10422
  }
10386
- const boundaryRow = geo.rowNumW > 0 && x < geo.rowNumW && y >= geo.headerH ? this._hitRowBoundary(y, geo) : null;
10423
+ const boundaryRow = geo.rowNumW > 0 && x < geo.rowNumW && y >= geo.headerH ? this._hitRowBoundary(y, geo, RESIZE_HIT_W_TOUCH) : null;
10387
10424
  if (boundaryRow !== null) {
10388
10425
  this._rowResize = { row: boundaryRow, startY: y, startHeight: this._rowLayout.heightOf(boundaryRow), undoBefore: this._snapshotStructural() };
10389
10426
  e.preventDefault();
@@ -10405,7 +10442,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10405
10442
  }
10406
10443
  return;
10407
10444
  }
10408
- if (this._sel && this._hitFillHandle(x, y, geo)) {
10445
+ if (this._sel && this._hitFillHandle(x, y, geo, RESIZE_HIT_W_TOUCH)) {
10409
10446
  this._fillDrag = { sel: this._sel };
10410
10447
  this._fillPreview = null;
10411
10448
  e.preventDefault();
@@ -10443,6 +10480,14 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10443
10480
  e.preventDefault();
10444
10481
  return;
10445
10482
  }
10483
+ if (this._drag) {
10484
+ if (!e.touches.length) return;
10485
+ const t = e.touches[0];
10486
+ const rect2 = this._canvas.getBoundingClientRect();
10487
+ this._updateScrollbarDrag(t.clientX - rect2.left, t.clientY - rect2.top);
10488
+ e.preventDefault();
10489
+ return;
10490
+ }
10446
10491
  if (!this._touch) return;
10447
10492
  const rect = this._canvas.getBoundingClientRect();
10448
10493
  if (e.touches.length >= 2 && this._touch.twoFinger) {
@@ -10479,6 +10524,10 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
10479
10524
  ev.touchend = (e) => {
10480
10525
  this._cancelLongPress();
10481
10526
  if (this._finalizeStructuralGesture()) return;
10527
+ if (this._drag) {
10528
+ this._drag = null;
10529
+ return;
10530
+ }
10482
10531
  if (this._rowTap) {
10483
10532
  const { row, ctrlKey, shiftKey } = this._rowTap;
10484
10533
  this._rowTap = null;
@@ -11908,7 +11957,7 @@ ${title ? `<h2>${esc(title)}</h2>` : ""}
11908
11957
  JHGrid.use(RowSelectionPlugin);
11909
11958
 
11910
11959
  // index.js
11911
- var VERSION = "0.1.0";
11960
+ var VERSION = "0.1.1";
11912
11961
  var SUPPORTED_BROWSERS = {
11913
11962
  chrome: 99,
11914
11963
  edge: 99,