photon-grid-core 2.0.0 → 2.0.4

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.
Files changed (175) hide show
  1. package/README.md +3 -3
  2. package/cell-selection/cell-selection-engine.d.ts.map +1 -1
  3. package/cell-selection/cell-selection-engine.js +11 -9
  4. package/cell-selection/cell-selection-engine.js.map +1 -1
  5. package/chart/range-chart-controller.d.ts +10 -0
  6. package/chart/range-chart-controller.d.ts.map +1 -1
  7. package/chart/range-chart-controller.js +22 -2
  8. package/chart/range-chart-controller.js.map +1 -1
  9. package/column-groups/column-group-drag-handler.d.ts.map +1 -1
  10. package/column-groups/column-group-drag-handler.js +64 -25
  11. package/column-groups/column-group-drag-handler.js.map +1 -1
  12. package/column-groups/column-group-header-builder.d.ts.map +1 -1
  13. package/column-groups/column-group-header-builder.js +7 -5
  14. package/column-groups/column-group-header-builder.js.map +1 -1
  15. package/column-groups/display-group-drag-handler.d.ts +7 -3
  16. package/column-groups/display-group-drag-handler.d.ts.map +1 -1
  17. package/column-groups/display-group-drag-handler.js +46 -7
  18. package/column-groups/display-group-drag-handler.js.map +1 -1
  19. package/column-groups/display-group-header-builder.d.ts +1 -1
  20. package/column-groups/display-group-header-builder.d.ts.map +1 -1
  21. package/column-groups/display-group-header-builder.js +9 -7
  22. package/column-groups/display-group-header-builder.js.map +1 -1
  23. package/core/column-model.d.ts +88 -5
  24. package/core/column-model.d.ts.map +1 -1
  25. package/core/column-model.js +215 -3
  26. package/core/column-model.js.map +1 -1
  27. package/core/create-grid.d.ts +54 -0
  28. package/core/create-grid.d.ts.map +1 -0
  29. package/core/create-grid.js +70 -0
  30. package/core/create-grid.js.map +1 -0
  31. package/core/grid-api.d.ts +10 -2
  32. package/core/grid-api.d.ts.map +1 -1
  33. package/core/grid-api.js +10 -0
  34. package/core/grid-api.js.map +1 -1
  35. package/core/grid-core.d.ts +7 -0
  36. package/core/grid-core.d.ts.map +1 -1
  37. package/core/grid-core.js +29 -8
  38. package/core/grid-core.js.map +1 -1
  39. package/core/grid-store.d.ts +5 -5
  40. package/core/grid-store.d.ts.map +1 -1
  41. package/core/pointer-utils.d.ts +59 -0
  42. package/core/pointer-utils.d.ts.map +1 -0
  43. package/core/pointer-utils.js +59 -0
  44. package/core/pointer-utils.js.map +1 -0
  45. package/drag-drop/drag-drop-engine.d.ts.map +1 -1
  46. package/drag-drop/drag-drop-engine.js +8 -8
  47. package/drag-drop/drag-drop-engine.js.map +1 -1
  48. package/engines/editing/cell-editor-engine.js +1 -1
  49. package/engines/editing/cell-editor-engine.js.map +1 -1
  50. package/index.d.ts +8 -3
  51. package/index.d.ts.map +1 -1
  52. package/index.js +4 -1
  53. package/index.js.map +1 -1
  54. package/package.json +1 -1
  55. package/photon-ai/index.d.ts +2 -0
  56. package/photon-ai/index.d.ts.map +1 -1
  57. package/photon-ai/index.js +2 -0
  58. package/photon-ai/index.js.map +1 -1
  59. package/photon-ai/photon-ai-panel.d.ts +40 -0
  60. package/photon-ai/photon-ai-panel.d.ts.map +1 -1
  61. package/photon-ai/photon-ai-panel.js +163 -8
  62. package/photon-ai/photon-ai-panel.js.map +1 -1
  63. package/photon-ai/photon-ai-service.d.ts +38 -1
  64. package/photon-ai/photon-ai-service.d.ts.map +1 -1
  65. package/photon-ai/photon-ai-service.js +72 -1
  66. package/photon-ai/photon-ai-service.js.map +1 -1
  67. package/photon-ai/provider/ai-provider.types.d.ts +174 -0
  68. package/photon-ai/provider/ai-provider.types.d.ts.map +1 -0
  69. package/photon-ai/provider/ai-provider.types.js +42 -0
  70. package/photon-ai/provider/ai-provider.types.js.map +1 -0
  71. package/photon-ai/provider/command-normalizer.d.ts +36 -0
  72. package/photon-ai/provider/command-normalizer.d.ts.map +1 -0
  73. package/photon-ai/provider/command-normalizer.js +120 -0
  74. package/photon-ai/provider/command-normalizer.js.map +1 -0
  75. package/photon-ai/provider/grid-context-builder.d.ts +37 -0
  76. package/photon-ai/provider/grid-context-builder.d.ts.map +1 -0
  77. package/photon-ai/provider/grid-context-builder.js +96 -0
  78. package/photon-ai/provider/grid-context-builder.js.map +1 -0
  79. package/photon-ai/provider/http-ai-provider.d.ts +42 -0
  80. package/photon-ai/provider/http-ai-provider.d.ts.map +1 -0
  81. package/photon-ai/provider/http-ai-provider.js +125 -0
  82. package/photon-ai/provider/http-ai-provider.js.map +1 -0
  83. package/photon-ai/provider/index.d.ts +30 -0
  84. package/photon-ai/provider/index.d.ts.map +1 -0
  85. package/photon-ai/provider/index.js +42 -0
  86. package/photon-ai/provider/index.js.map +1 -0
  87. package/photon-ai/provider/presets.d.ts +29 -0
  88. package/photon-ai/provider/presets.d.ts.map +1 -0
  89. package/photon-ai/provider/presets.js +110 -0
  90. package/photon-ai/provider/presets.js.map +1 -0
  91. package/photon-ai/provider/response-parsing.d.ts +22 -0
  92. package/photon-ai/provider/response-parsing.d.ts.map +1 -0
  93. package/photon-ai/provider/response-parsing.js +79 -0
  94. package/photon-ai/provider/response-parsing.js.map +1 -0
  95. package/photon-ai/provider/system-prompt.d.ts +26 -0
  96. package/photon-ai/provider/system-prompt.d.ts.map +1 -0
  97. package/photon-ai/provider/system-prompt.js +82 -0
  98. package/photon-ai/provider/system-prompt.js.map +1 -0
  99. package/photon-grid.min.js +497 -56
  100. package/renderer/body-renderer.d.ts +23 -0
  101. package/renderer/body-renderer.d.ts.map +1 -1
  102. package/renderer/body-renderer.js +102 -24
  103. package/renderer/body-renderer.js.map +1 -1
  104. package/renderer/column-chooser.d.ts +82 -0
  105. package/renderer/column-chooser.d.ts.map +1 -0
  106. package/renderer/column-chooser.js +335 -0
  107. package/renderer/column-chooser.js.map +1 -0
  108. package/renderer/column-menu.d.ts.map +1 -1
  109. package/renderer/column-menu.js +32 -20
  110. package/renderer/column-menu.js.map +1 -1
  111. package/renderer/column-style-manager.d.ts +27 -0
  112. package/renderer/column-style-manager.d.ts.map +1 -1
  113. package/renderer/column-style-manager.js +47 -0
  114. package/renderer/column-style-manager.js.map +1 -1
  115. package/renderer/detail-row-renderer.d.ts.map +1 -1
  116. package/renderer/detail-row-renderer.js +8 -6
  117. package/renderer/detail-row-renderer.js.map +1 -1
  118. package/renderer/grid-renderer.d.ts +10 -0
  119. package/renderer/grid-renderer.d.ts.map +1 -1
  120. package/renderer/grid-renderer.js +64 -20
  121. package/renderer/grid-renderer.js.map +1 -1
  122. package/renderer/group-drop-zone.js +14 -14
  123. package/renderer/group-drop-zone.js.map +1 -1
  124. package/renderer/header-renderer.d.ts +65 -0
  125. package/renderer/header-renderer.d.ts.map +1 -1
  126. package/renderer/header-renderer.js +347 -44
  127. package/renderer/header-renderer.js.map +1 -1
  128. package/renderer/row-animator.js +3 -3
  129. package/renderer/row-drag-renderer.js +6 -6
  130. package/renderer/row-drag-renderer.js.map +1 -1
  131. package/renderer/scroll-controller.d.ts +40 -0
  132. package/renderer/scroll-controller.d.ts.map +1 -1
  133. package/renderer/scroll-controller.js +184 -0
  134. package/renderer/scroll-controller.js.map +1 -1
  135. package/styles/base/column-chooser.css.d.ts +13 -0
  136. package/styles/base/column-chooser.css.d.ts.map +1 -0
  137. package/styles/base/column-chooser.css.js +241 -0
  138. package/styles/base/column-chooser.css.js.map +1 -0
  139. package/styles/base/column-context-menu.css.d.ts +1 -1
  140. package/styles/base/column-context-menu.css.d.ts.map +1 -1
  141. package/styles/base/column-context-menu.css.js +10 -0
  142. package/styles/base/column-context-menu.css.js.map +1 -1
  143. package/styles/base/filter.css.d.ts +1 -1
  144. package/styles/base/filter.css.js +1 -1
  145. package/styles/base/header.css.d.ts +1 -1
  146. package/styles/base/header.css.d.ts.map +1 -1
  147. package/styles/base/header.css.js +19 -0
  148. package/styles/base/header.css.js.map +1 -1
  149. package/styles/base/photon-ai.css.d.ts +1 -1
  150. package/styles/base/photon-ai.css.d.ts.map +1 -1
  151. package/styles/base/photon-ai.css.js +65 -0
  152. package/styles/base/photon-ai.css.js.map +1 -1
  153. package/styles/base/scrollbars.css.d.ts +1 -1
  154. package/styles/base/scrollbars.css.d.ts.map +1 -1
  155. package/styles/base/scrollbars.css.js +50 -4
  156. package/styles/base/scrollbars.css.js.map +1 -1
  157. package/styles/base/touch.css.d.ts +23 -0
  158. package/styles/base/touch.css.d.ts.map +1 -0
  159. package/styles/base/touch.css.js +85 -0
  160. package/styles/base/touch.css.js.map +1 -0
  161. package/styles/base-styles.d.ts.map +1 -1
  162. package/styles/base-styles.js +4 -0
  163. package/styles/base-styles.js.map +1 -1
  164. package/styles/themes/theme-dark.d.ts +1 -1
  165. package/styles/themes/theme-dark.d.ts.map +1 -1
  166. package/styles/themes/theme-dark.js +3 -0
  167. package/styles/themes/theme-dark.js.map +1 -1
  168. package/types/column.types.d.ts +36 -0
  169. package/types/column.types.d.ts.map +1 -1
  170. package/types/grid.types.d.ts +3 -2
  171. package/types/grid.types.d.ts.map +1 -1
  172. package/types/photon-ai.types.d.ts +125 -5
  173. package/types/photon-ai.types.d.ts.map +1 -1
  174. package/types/photon-ai.types.js +18 -1
  175. package/types/photon-ai.types.js.map +1 -1
@@ -3,9 +3,16 @@ import { ColumnMenu } from './column-menu';
3
3
  import { GroupContextMenu } from './group-context-menu';
4
4
  import { createDiv, toggleClass } from './dom-utils';
5
5
  import { resolveColumnRenderer } from './renderer-resolver';
6
+ import { isTouchPointer, DRAG_THRESHOLD_MOUSE, DRAG_THRESHOLD_TOUCH, LONG_PRESS_MS, } from '../core/pointer-utils';
6
7
  export class HeaderRenderer {
7
8
  /** Read by grid-renderer's columns-store watcher to skip header destroy during drag */
8
9
  get isDraggingCol() { return this.isDragging; }
10
+ /**
11
+ * `true` while the header owns the pointer for a column reorder or resize.
12
+ * The ScrollController's touch-pan yields to this (see `setGestureGuard`) so a
13
+ * long-press reorder or an edge resize is never fought by kinetic scrolling.
14
+ */
15
+ get isBusy() { return this.isDragging || this._isResizingColumn; }
9
16
  /**
10
17
  * `true` between the resize-handle `mousedown` and the corresponding `mouseup`.
11
18
  * Grid-renderer uses this to suppress body `renderRows` during resize — column
@@ -91,8 +98,15 @@ export class HeaderRenderer {
91
98
  this.lastDragClientX = 0;
92
99
  /** Callback wired by GridRenderer to open a filter panel for a column. */
93
100
  this.openFilterPanelFn = null;
101
+ /** Callback wired by GridRenderer to open the Column Chooser dialog. */
102
+ this.openColumnChooserFn = null;
103
+ /** Callback wired by GridRenderer to re-run the data pipeline (e.g. after an aggregate change). */
104
+ this.columnDataRefreshFn = null;
94
105
  /** True while the user holds the resize handle and is actively moving the mouse. */
95
106
  this._isResizingColumn = false;
107
+ /** Column id and timestamp of the last resize-handle press, for double-click (auto-size) detection. */
108
+ this.lastResizeDownColId = '';
109
+ this.lastResizeDownAt = 0;
96
110
  // ─── Auto-scroll ──────────────────────────────────────────────────────────
97
111
  this.tickAutoScroll = () => {
98
112
  if (!this.isDragging || this.autoScrollSpeed === 0)
@@ -114,11 +128,11 @@ export class HeaderRenderer {
114
128
  };
115
129
  this.columnMenu = new ColumnMenu(columnModel, sortEngine, eventBus, iconRenderer, (action, colId) => this.onAction(action, colId));
116
130
  this.columnMenu.setMenuCallbacks({
117
- onAutoSize: (colId) => this.onAction('autosize', colId),
118
- onAutoSizeAll: () => this.onAction('autosize-all', ''),
119
- onFitToGrid: () => this.onAction('fit-to-grid', ''),
120
- onResetWidth: (colId) => this.onAction('reset-width', colId),
121
- onOpenColumnChooser: () => this.onAction('column-chooser', ''),
131
+ onAutoSize: (colId) => { this.autoSizeColumns([colId]); this.onAction('autosize', colId); },
132
+ onAutoSizeAll: () => { this.autoSizeColumns(this.columnModel.getVisibleColumns().map((c) => c.colId)); this.onAction('autosize-all', ''); },
133
+ onFitToGrid: () => { this.fitColumnsToGrid(); this.onAction('fit-to-grid', ''); },
134
+ onResetWidth: (colId) => { this.resetColumnWidth(colId); this.onAction('reset-width', colId); },
135
+ onOpenColumnChooser: () => { this.openColumnChooserFn?.(); this.onAction('column-chooser', ''); },
122
136
  onOpenAdvancedFilter: (colDef, anchorEl) => {
123
137
  this.openFilterPanelFn?.(colDef, anchorEl);
124
138
  this.onAction('advanced-filter', colDef.colId);
@@ -130,12 +144,26 @@ export class HeaderRenderer {
130
144
  },
131
145
  onCopyColumn: (colDef) => this.onAction('copy-column', colDef.colId),
132
146
  onCopyValues: (colDef) => this.onAction('copy-values', colDef.colId),
133
- onRename: (colDef) => this.onAction('rename', colDef.colId),
134
- onDuplicate: (colDef) => this.onAction('duplicate', colDef.colId),
135
- onFreezePosition: (colDef) => this.onAction('freeze', colDef.colId),
136
- onLockColumn: (colDef) => this.onAction('lock', colDef.colId),
137
- onResetColumn: (colDef) => this.onAction('reset-column', colDef.colId),
138
- onAggregate: (colDef, func) => this.onAction(`aggregate-${func}`, colDef.colId),
147
+ onRename: (colDef) => { this.startHeaderRename(colDef.colId); this.onAction('rename', colDef.colId); },
148
+ onDuplicate: (colDef) => { this.columnModel.duplicateColumn(colDef.colId); this.onAction('duplicate', colDef.colId); },
149
+ onFreezePosition: (colDef) => { this.columnModel.toggleColumnFrozen(colDef.colId); this.onAction('freeze', colDef.colId); },
150
+ onLockColumn: (colDef) => { this.columnModel.toggleColumnLocked(colDef.colId); this.onAction('lock', colDef.colId); },
151
+ onResetColumn: (colDef) => { this.colStyles.clearUserWidth(colDef.colId); this.columnModel.resetColumn(colDef.colId); this.onAction('reset-column', colDef.colId); },
152
+ onAggregate: (colDef, func) => {
153
+ // Toggle: re-selecting the current function clears it.
154
+ const f = func;
155
+ const current = this.columnModel.getColumn(colDef.colId)?.aggFunc;
156
+ this.columnModel.setColumnAggFunc(colDef.colId, current === f ? null : f);
157
+ // aggFunc only affects the *grouped* aggregation display, so a data
158
+ // pipeline refresh is only needed when grouping is active. When it isn't,
159
+ // the change has no visible effect yet (it applies the next time rows are
160
+ // grouped) — so we skip refresh() to avoid rebuilding unrelated live
161
+ // views such as linked range charts, which watch `visibleRows`.
162
+ if (this.store.get('groupedColumnIds').length > 0) {
163
+ this.columnDataRefreshFn?.();
164
+ }
165
+ this.onAction(`aggregate-${func}`, colDef.colId);
166
+ },
139
167
  onMoveLeft: (colId) => {
140
168
  const cols = columnModel.getVisibleColumns();
141
169
  const idx = cols.findIndex((c) => c.colId === colId);
@@ -171,6 +199,14 @@ export class HeaderRenderer {
171
199
  });
172
200
  this.boundMouseMove = this.onGlobalMouseMove.bind(this);
173
201
  this.boundMouseUp = this.onGlobalMouseUp.bind(this);
202
+ // Keep the sort arrows in sync in place. Sorting no longer rebuilds the
203
+ // header (that teardown fought the row animation), so the indicator is
204
+ // updated directly here — covering both header clicks and programmatic
205
+ // sorts. `colId === ''` (clear-all) clears every arrow.
206
+ this.eventBus.on(GridEventType.COLUMN_SORTED, (payload) => {
207
+ const p = payload;
208
+ this.updateSortIndicator(p.colId, p.order);
209
+ });
174
210
  }
175
211
  setScrollCallback(fn, canScrollX) {
176
212
  this.scrollByX = fn;
@@ -188,6 +224,25 @@ export class HeaderRenderer {
188
224
  setOpenFilterPanelCallback(fn) {
189
225
  this.openFilterPanelFn = fn;
190
226
  }
227
+ /**
228
+ * Registers the callback that opens the Column Chooser dialog, invoked from
229
+ * the column menu's and group menu's "Column Chooser…" items.
230
+ *
231
+ * @param fn - Opens the chooser (owned by `GridRenderer`).
232
+ */
233
+ setColumnChooserCallback(fn) {
234
+ this.openColumnChooserFn = fn;
235
+ }
236
+ /**
237
+ * Registers a callback that re-runs the grid's data pipeline (filter → sort →
238
+ * group → aggregate). Invoked after a column aggregate function changes so
239
+ * group aggregations recompute immediately when grouping is active.
240
+ *
241
+ * @param fn - Runs the pipeline + render (typically `GridApi.refresh`).
242
+ */
243
+ setColumnDataRefreshCallback(fn) {
244
+ this.columnDataRefreshFn = fn;
245
+ }
191
246
  /**
192
247
  * Wire the column-group model and DOM builder into the header renderer.
193
248
  *
@@ -214,7 +269,7 @@ export class HeaderRenderer {
214
269
  this.displayGroupEngine = engine;
215
270
  this.groupContextMenu = new GroupContextMenu(engine, this.iconRenderer);
216
271
  this.groupContextMenu.setCallbacks({
217
- onOpenColumnChooser: () => this.onAction('column-chooser', ''),
272
+ onOpenColumnChooser: () => { this.openColumnChooserFn?.(); this.onAction('column-chooser', ''); },
218
273
  onAction: (action, groupId) => this.onAction(action, groupId),
219
274
  });
220
275
  }
@@ -397,13 +452,19 @@ export class HeaderRenderer {
397
452
  for (const row of rows) {
398
453
  for (const cell of Array.from(row.querySelectorAll('.pg-th[data-col-id]'))) {
399
454
  const isTarget = cell.getAttribute('data-col-id') === colId;
455
+ const wasSorted = cell.classList.contains('pg-th--sorted');
400
456
  toggleClass(cell, 'pg-th--sort-asc', isTarget && order === 'asc');
401
457
  toggleClass(cell, 'pg-th--sort-desc', isTarget && order === 'desc');
402
458
  toggleClass(cell, 'pg-th--sorted', isTarget && order !== null);
459
+ const iconEl = cell.querySelector('.pg-th__sort-icon');
460
+ if (!iconEl)
461
+ continue;
403
462
  if (isTarget) {
404
- const iconEl = cell.querySelector('.pg-th__sort-icon');
405
- if (iconEl)
406
- this.iconRenderer.updateIcon(iconEl, order === 'asc' ? 'sortAsc' : order === 'desc' ? 'sortDesc' : 'sortNone');
463
+ this.iconRenderer.updateIcon(iconEl, order === 'asc' ? 'sortAsc' : order === 'desc' ? 'sortDesc' : 'sortNone');
464
+ }
465
+ else if (wasSorted) {
466
+ // A different column just became the sort target — clear this one's arrow.
467
+ this.iconRenderer.updateIcon(iconEl, 'sortNone');
407
468
  }
408
469
  }
409
470
  }
@@ -423,8 +484,8 @@ export class HeaderRenderer {
423
484
  });
424
485
  }
425
486
  destroy() {
426
- document.removeEventListener('mousemove', this.boundMouseMove);
427
- document.removeEventListener('mouseup', this.boundMouseUp);
487
+ document.removeEventListener('pointermove', this.boundMouseMove);
488
+ document.removeEventListener('pointerup', this.boundMouseUp);
428
489
  this.columnMenu.destroy();
429
490
  this.groupContextMenu?.destroy();
430
491
  this.leftHeaderRowEl = null;
@@ -658,7 +719,27 @@ export class HeaderRenderer {
658
719
  if (col.resizable !== false) {
659
720
  const resizeHandle = createDiv('pg-th__resize-handle');
660
721
  // resizeHandle.innerHTML = '|';
661
- resizeHandle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); this.startResize(e, col, th); });
722
+ resizeHandle.addEventListener('pointerdown', (e) => {
723
+ if (e.button !== 0)
724
+ return;
725
+ e.preventDefault();
726
+ e.stopPropagation();
727
+ // Double-press on the same handle → auto-size the column (AG Grid
728
+ // convention). Detected by timing rather than the native `dblclick`
729
+ // event: the first press's mouseup can rebuild the header
730
+ // (COLUMNS_STATE_CHANGED) and replace this element, so a native dblclick
731
+ // would never fire on the original node. Keying on colId survives that.
732
+ if (this.lastResizeDownColId === col.colId && e.timeStamp - this.lastResizeDownAt < 350) {
733
+ this.lastResizeDownColId = '';
734
+ this.lastResizeDownAt = 0;
735
+ this.autoSizeColumns([col.colId]);
736
+ this.onAction('autosize', col.colId);
737
+ return;
738
+ }
739
+ this.lastResizeDownColId = col.colId;
740
+ this.lastResizeDownAt = e.timeStamp;
741
+ this.startResize(e, col, th);
742
+ });
662
743
  th.appendChild(resizeHandle);
663
744
  }
664
745
  if (col.sortable !== false) {
@@ -710,30 +791,71 @@ export class HeaderRenderer {
710
791
  return row;
711
792
  }
712
793
  attachColumnDragListeners(th, col, _panelColumns, panelRowEl) {
713
- th.addEventListener('mousedown', (e) => {
794
+ th.addEventListener('pointerdown', (e) => {
714
795
  if (e.target.closest('.pg-th__resize-handle, .pg-th__menu-btn, .pg-th__filter-btn, .pg-checkbox'))
715
796
  return;
716
797
  if (col.draggable === false || e.button !== 0)
717
798
  return;
718
799
  const startX = e.clientX;
719
- let moved = false;
720
- const onMoveCheck = (ev) => {
721
- if (!moved && Math.abs(ev.clientX - startX) > 5) {
722
- moved = true;
723
- this.startColumnDrag(col, panelRowEl);
724
- document.removeEventListener('mousemove', onMoveCheck);
725
- document.addEventListener('mousemove', this.boundMouseMove);
726
- document.addEventListener('mouseup', this.boundMouseUp);
800
+ const startY = e.clientY;
801
+ const touch = isTouchPointer(e);
802
+ let started = false; // actual column drag engaged
803
+ let longPressTimer = 0;
804
+ // Tear down the pre-drag probe listeners + long-press timer + affordance.
805
+ const cleanupProbe = () => {
806
+ if (longPressTimer) {
807
+ clearTimeout(longPressTimer);
808
+ longPressTimer = 0;
727
809
  }
728
- if (moved)
729
- this.onDragMove(ev.clientX);
810
+ th.classList.remove('pg-th--drag-armed');
811
+ document.removeEventListener('pointermove', onMoveCheck);
812
+ document.removeEventListener('pointerup', onUpEarly);
730
813
  };
731
- const onUpEarly = () => {
732
- document.removeEventListener('mousemove', onMoveCheck);
733
- document.removeEventListener('mouseup', onUpEarly);
814
+ // Promote the press to a live column drag and hand movement over to the
815
+ // global drag handlers. Called by the mouse threshold or the touch
816
+ // long-press timer.
817
+ const beginDrag = () => {
818
+ if (started)
819
+ return;
820
+ started = true;
821
+ if (longPressTimer) {
822
+ clearTimeout(longPressTimer);
823
+ longPressTimer = 0;
824
+ }
825
+ th.classList.remove('pg-th--drag-armed');
826
+ this.startColumnDrag(col, panelRowEl);
827
+ document.removeEventListener('pointermove', onMoveCheck);
828
+ document.addEventListener('pointermove', this.boundMouseMove);
829
+ document.addEventListener('pointerup', this.boundMouseUp);
830
+ this.onDragMove(startX);
734
831
  };
735
- document.addEventListener('mousemove', onMoveCheck);
736
- document.addEventListener('mouseup', onUpEarly);
832
+ const onMoveCheck = (ev) => {
833
+ if (started)
834
+ return; // global handlers own movement now
835
+ const dx = Math.abs(ev.clientX - startX);
836
+ const dy = Math.abs(ev.clientY - startY);
837
+ if (touch) {
838
+ // Movement before the long-press fires means the user is swiping to
839
+ // scroll, not reordering — abandon so the ScrollController pans.
840
+ if (dx > DRAG_THRESHOLD_TOUCH || dy > DRAG_THRESHOLD_TOUCH)
841
+ cleanupProbe();
842
+ }
843
+ else if (dx > DRAG_THRESHOLD_MOUSE) {
844
+ beginDrag();
845
+ }
846
+ };
847
+ const onUpEarly = () => { if (!started)
848
+ cleanupProbe(); };
849
+ document.addEventListener('pointermove', onMoveCheck);
850
+ document.addEventListener('pointerup', onUpEarly);
851
+ if (touch) {
852
+ // Arm a long-press: hold ~400ms without swiping to pick the column up.
853
+ th.classList.add('pg-th--drag-armed');
854
+ longPressTimer = window.setTimeout(() => {
855
+ longPressTimer = 0;
856
+ beginDrag();
857
+ }, LONG_PRESS_MS);
858
+ }
737
859
  });
738
860
  }
739
861
  startColumnDrag(col, panelRowEl) {
@@ -1279,8 +1401,8 @@ export class HeaderRenderer {
1279
1401
  this.draggingPanelRowEl = null;
1280
1402
  document.body.style.cursor = '';
1281
1403
  document.body.style.userSelect = '';
1282
- document.removeEventListener('mousemove', this.boundMouseMove);
1283
- document.removeEventListener('mouseup', this.boundMouseUp);
1404
+ document.removeEventListener('pointermove', this.boundMouseMove);
1405
+ document.removeEventListener('pointerup', this.boundMouseUp);
1284
1406
  }
1285
1407
  startResize(e, col, thEl) {
1286
1408
  const startX = e.clientX;
@@ -1292,11 +1414,27 @@ export class HeaderRenderer {
1292
1414
  // their right-edge handle: dragging right widens.
1293
1415
  const dir = col.pinned === 'right' ? -1 : 1;
1294
1416
  const minWidth = col.minWidth ?? 60;
1295
- thEl.classList.add('pg-th--resizing');
1296
1417
  document.body.style.cursor = 'col-resize';
1297
1418
  document.body.style.userSelect = 'none';
1298
1419
  this._isResizingColumn = true;
1420
+ // Only treat this as a resize once the pointer actually moves past a small
1421
+ // threshold. A bare click (e.g. the first press of a double-click) must not
1422
+ // resize, rebuild the header, or freeze flex columns — otherwise the handle
1423
+ // element is replaced mid-gesture and the double-click auto-size is lost.
1424
+ let moved = false;
1299
1425
  const onMove = (ev) => {
1426
+ if (!moved) {
1427
+ if (Math.abs(ev.clientX - startX) <= 2)
1428
+ return;
1429
+ moved = true;
1430
+ thEl.classList.add('pg-th--resizing');
1431
+ // Lock every flex column to its current pixel width so dragging this one
1432
+ // resizes only it — the others keep their widths (the total may overflow
1433
+ // into a horizontal scrollbar) instead of flex redistributing and
1434
+ // collapsing them to minWidth. Done here (not on mousedown) so a plain
1435
+ // click never silently converts flex columns to fixed.
1436
+ this.colStyles.freezeFlexWidths();
1437
+ }
1300
1438
  this.colStyles.setWidth(col.colId, Math.max(minWidth, startWidth + (ev.clientX - startX) * dir));
1301
1439
  this.onResizeCb?.();
1302
1440
  };
@@ -1304,17 +1442,19 @@ export class HeaderRenderer {
1304
1442
  // Clear the flag BEFORE setColumnWidth fires an event so the final
1305
1443
  // post-resize render calls renderRows normally and the body is correct.
1306
1444
  this._isResizingColumn = false;
1307
- const newWidth = Math.max(minWidth, startWidth + (ev.clientX - startX) * dir);
1308
- this.colStyles.setWidth(col.colId, newWidth);
1309
- this.columnModel.setColumnWidth(col.colId, newWidth, true);
1310
1445
  thEl.classList.remove('pg-th--resizing');
1311
1446
  document.body.style.cursor = '';
1312
1447
  document.body.style.userSelect = '';
1313
- document.removeEventListener('mousemove', onMove);
1314
- document.removeEventListener('mouseup', onUp);
1448
+ document.removeEventListener('pointermove', onMove);
1449
+ document.removeEventListener('pointerup', onUp);
1450
+ if (!moved)
1451
+ return; // no drag occurred — leave widths untouched
1452
+ const newWidth = Math.max(minWidth, startWidth + (ev.clientX - startX) * dir);
1453
+ this.colStyles.setWidth(col.colId, newWidth);
1454
+ this.columnModel.setColumnWidth(col.colId, newWidth, true);
1315
1455
  };
1316
- document.addEventListener('mousemove', onMove);
1317
- document.addEventListener('mouseup', onUp);
1456
+ document.addEventListener('pointermove', onMove);
1457
+ document.addEventListener('pointerup', onUp);
1318
1458
  }
1319
1459
  /**
1320
1460
  * Returns `true` when `colId` (at its new position `newIdx` in the visible
@@ -1338,6 +1478,169 @@ export class HeaderRenderer {
1338
1478
  const right = cols[newIdx + 1];
1339
1479
  return !(left && siblings.has(left.colId)) && !(right && siblings.has(right.colId));
1340
1480
  }
1481
+ // ──────────────────── Column menu: resize operations ────────────────────
1482
+ /** Resolves the owning `.pg-grid` element from any live header row, or `null`. */
1483
+ getGridEl() {
1484
+ return (this.centerHeaderRowEl ?? this.leftHeaderRowEl ?? this.rightHeaderRowEl)
1485
+ ?.closest('.pg-grid') ?? null;
1486
+ }
1487
+ /**
1488
+ * Starts an inline rename of a column: replaces the header label with a text
1489
+ * input, committing on Enter/blur and cancelling on Escape. Committing calls
1490
+ * {@link ColumnModel.setColumnHeader}, which rebuilds the header with the new
1491
+ * text. A no-op when the header cell can't be found.
1492
+ */
1493
+ startHeaderRename(colId) {
1494
+ const gridEl = this.getGridEl();
1495
+ const th = gridEl?.querySelector(`.pg-th[data-col-id="${colId}"]`);
1496
+ const label = th?.querySelector('.pg-th__label');
1497
+ const col = this.columnModel.getColumn(colId);
1498
+ if (!th || !label || !col)
1499
+ return;
1500
+ const input = document.createElement('input');
1501
+ input.className = 'pg-th__rename-input';
1502
+ input.type = 'text';
1503
+ input.value = col.header;
1504
+ input.setAttribute('aria-label', 'Rename column');
1505
+ let done = false;
1506
+ const commit = () => {
1507
+ if (done)
1508
+ return;
1509
+ done = true;
1510
+ const value = input.value.trim();
1511
+ if (value && value !== col.header) {
1512
+ this.columnModel.setColumnHeader(colId, value); // triggers header rebuild
1513
+ }
1514
+ else {
1515
+ input.replaceWith(label); // unchanged → restore original label
1516
+ }
1517
+ };
1518
+ const cancel = () => {
1519
+ if (done)
1520
+ return;
1521
+ done = true;
1522
+ input.replaceWith(label);
1523
+ };
1524
+ input.addEventListener('keydown', (e) => {
1525
+ e.stopPropagation();
1526
+ if (e.key === 'Enter') {
1527
+ e.preventDefault();
1528
+ commit();
1529
+ }
1530
+ else if (e.key === 'Escape') {
1531
+ e.preventDefault();
1532
+ cancel();
1533
+ }
1534
+ });
1535
+ input.addEventListener('blur', commit);
1536
+ // Don't let the input's own clicks reach the header's sort/drag handlers.
1537
+ input.addEventListener('mousedown', (e) => e.stopPropagation());
1538
+ input.addEventListener('click', (e) => e.stopPropagation());
1539
+ label.replaceWith(input);
1540
+ input.focus();
1541
+ input.select();
1542
+ }
1543
+ /**
1544
+ * Measures the pixel width needed to show a column's header plus every
1545
+ * currently-rendered cell without truncation, clamped to the column's
1546
+ * min/max. Only virtualized (on-screen) rows are measured, so the cost scales
1547
+ * with the viewport, not the dataset. Uses one reused off-screen `<span>` so
1548
+ * cell fonts/metrics match the grid.
1549
+ */
1550
+ measureColumnContentWidth(gridEl, col) {
1551
+ const measurer = gridEl.ownerDocument.createElement('span');
1552
+ measurer.style.cssText =
1553
+ 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;pointer-events:none;padding:0 12px;';
1554
+ gridEl.appendChild(measurer);
1555
+ measurer.textContent = col.header ?? '';
1556
+ let maxWidth = measurer.offsetWidth + 24; // header text + sort/menu affordance headroom
1557
+ const cells = gridEl.querySelectorAll(`.pg-cell[data-col-id="${col.colId}"]`);
1558
+ for (let i = 0; i < cells.length; i++) {
1559
+ measurer.textContent = cells[i].textContent ?? '';
1560
+ const w = measurer.offsetWidth + 24;
1561
+ if (w > maxWidth)
1562
+ maxWidth = w;
1563
+ }
1564
+ measurer.remove();
1565
+ const min = col.minWidth ?? 40;
1566
+ const max = col.maxWidth ?? Infinity;
1567
+ return Math.round(Math.min(max, Math.max(min, maxWidth)));
1568
+ }
1569
+ /**
1570
+ * Auto-sizes the given columns to their content width. Measures each, then
1571
+ * commits all widths in a single style flush + single render (via the batch
1572
+ * setters), so "Auto Size All" stays cheap. A no-op if the grid DOM or the
1573
+ * columns cannot be resolved.
1574
+ */
1575
+ autoSizeColumns(colIds) {
1576
+ const gridEl = this.getGridEl();
1577
+ if (!gridEl)
1578
+ return;
1579
+ const entries = [];
1580
+ for (const colId of colIds) {
1581
+ const col = this.columnModel.getColumn(colId);
1582
+ if (!col)
1583
+ continue;
1584
+ entries.push([colId, this.measureColumnContentWidth(gridEl, col)]);
1585
+ }
1586
+ if (entries.length === 0)
1587
+ return;
1588
+ // Style manager first (immediate CSS width + marks fixed so flex can't
1589
+ // override), then the model (persisted state + one COLUMNS_STATE_CHANGED
1590
+ // that drives the re-render and scrollbar/content-width recompute).
1591
+ this.colStyles.setWidths(entries);
1592
+ this.columnModel.setColumnWidths(entries);
1593
+ }
1594
+ /**
1595
+ * Distributes the center viewport width across the visible unpinned columns
1596
+ * so they exactly fill the grid with no horizontal scroll (proportional to
1597
+ * current widths, clamped to each column's min/max). Pinned columns keep
1598
+ * their widths. A no-op when the center viewport can't be measured.
1599
+ */
1600
+ fitColumnsToGrid() {
1601
+ const gridEl = this.getGridEl();
1602
+ if (!gridEl)
1603
+ return;
1604
+ const centerBody = gridEl.querySelector('.pg-panel--center .pg-panel__body');
1605
+ const available = centerBody?.clientWidth ?? 0;
1606
+ if (available <= 0)
1607
+ return;
1608
+ const centerCols = this.columnModel
1609
+ .getVisibleColumns()
1610
+ .filter((c) => c.pinned !== 'left' && c.pinned !== 'right');
1611
+ if (centerCols.length === 0)
1612
+ return;
1613
+ const totalCurrent = centerCols.reduce((sum, c) => sum + this.colStyles.getWidth(c.colId), 0) || 1;
1614
+ const entries = [];
1615
+ let allocated = 0;
1616
+ for (const c of centerCols) {
1617
+ const min = c.minWidth ?? 40;
1618
+ const max = c.maxWidth ?? Infinity;
1619
+ const w = Math.min(max, Math.max(min, Math.round((this.colStyles.getWidth(c.colId) / totalCurrent) * available)));
1620
+ entries.push([c.colId, w]);
1621
+ allocated += w;
1622
+ }
1623
+ // Absorb rounding/clamping drift into the last column so the total is exact.
1624
+ const diff = available - allocated;
1625
+ if (diff !== 0) {
1626
+ const lastCol = centerCols[centerCols.length - 1];
1627
+ const min = lastCol.minWidth ?? 40;
1628
+ const max = lastCol.maxWidth ?? Infinity;
1629
+ const last = entries[entries.length - 1];
1630
+ entries[entries.length - 1] = [last[0], Math.min(max, Math.max(min, last[1] + diff))];
1631
+ }
1632
+ this.colStyles.setWidths(entries);
1633
+ this.columnModel.setColumnWidths(entries);
1634
+ }
1635
+ /**
1636
+ * Resets a column's width to its original definition. Clears the user-fixed
1637
+ * override in the style manager (so a `flex` column resumes flexing) and
1638
+ * restores the model's width, which re-renders via `COLUMNS_STATE_CHANGED`.
1639
+ */
1640
+ resetColumnWidth(colId) {
1641
+ this.colStyles.clearUserWidth(colId);
1642
+ this.columnModel.resetColumnWidth(colId);
1643
+ }
1341
1644
  onAction(action, colId) {
1342
1645
  this.eventBus.emit(GridEventType.COLUMNS_STATE_CHANGED, {
1343
1646
  action, colId, states: this.columnModel.getColumnStates(),