photon-grid-core 2.0.1 → 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.
- package/README.md +3 -3
- package/cell-selection/cell-selection-engine.d.ts.map +1 -1
- package/cell-selection/cell-selection-engine.js +11 -9
- package/cell-selection/cell-selection-engine.js.map +1 -1
- package/chart/range-chart-controller.d.ts +10 -0
- package/chart/range-chart-controller.d.ts.map +1 -1
- package/chart/range-chart-controller.js +22 -2
- package/chart/range-chart-controller.js.map +1 -1
- package/column-groups/column-group-drag-handler.d.ts.map +1 -1
- package/column-groups/column-group-drag-handler.js +64 -25
- package/column-groups/column-group-drag-handler.js.map +1 -1
- package/column-groups/column-group-header-builder.d.ts.map +1 -1
- package/column-groups/column-group-header-builder.js +7 -5
- package/column-groups/column-group-header-builder.js.map +1 -1
- package/column-groups/display-group-drag-handler.d.ts +7 -3
- package/column-groups/display-group-drag-handler.d.ts.map +1 -1
- package/column-groups/display-group-drag-handler.js +46 -7
- package/column-groups/display-group-drag-handler.js.map +1 -1
- package/column-groups/display-group-header-builder.d.ts +1 -1
- package/column-groups/display-group-header-builder.d.ts.map +1 -1
- package/column-groups/display-group-header-builder.js +9 -7
- package/column-groups/display-group-header-builder.js.map +1 -1
- package/core/column-model.d.ts +88 -5
- package/core/column-model.d.ts.map +1 -1
- package/core/column-model.js +215 -3
- package/core/column-model.js.map +1 -1
- package/core/grid-api.d.ts +10 -2
- package/core/grid-api.d.ts.map +1 -1
- package/core/grid-api.js +10 -0
- package/core/grid-api.js.map +1 -1
- package/core/grid-core.d.ts +7 -0
- package/core/grid-core.d.ts.map +1 -1
- package/core/grid-core.js +29 -8
- package/core/grid-core.js.map +1 -1
- package/core/grid-store.d.ts +5 -5
- package/core/grid-store.d.ts.map +1 -1
- package/core/pointer-utils.d.ts +59 -0
- package/core/pointer-utils.d.ts.map +1 -0
- package/core/pointer-utils.js +59 -0
- package/core/pointer-utils.js.map +1 -0
- package/drag-drop/drag-drop-engine.d.ts.map +1 -1
- package/drag-drop/drag-drop-engine.js +8 -8
- package/drag-drop/drag-drop-engine.js.map +1 -1
- package/engines/editing/cell-editor-engine.js +1 -1
- package/engines/editing/cell-editor-engine.js.map +1 -1
- package/index.d.ts +6 -3
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/photon-ai/index.d.ts +2 -0
- package/photon-ai/index.d.ts.map +1 -1
- package/photon-ai/index.js +2 -0
- package/photon-ai/index.js.map +1 -1
- package/photon-ai/photon-ai-panel.d.ts +40 -0
- package/photon-ai/photon-ai-panel.d.ts.map +1 -1
- package/photon-ai/photon-ai-panel.js +163 -8
- package/photon-ai/photon-ai-panel.js.map +1 -1
- package/photon-ai/photon-ai-service.d.ts +38 -1
- package/photon-ai/photon-ai-service.d.ts.map +1 -1
- package/photon-ai/photon-ai-service.js +72 -1
- package/photon-ai/photon-ai-service.js.map +1 -1
- package/photon-ai/provider/ai-provider.types.d.ts +174 -0
- package/photon-ai/provider/ai-provider.types.d.ts.map +1 -0
- package/photon-ai/provider/ai-provider.types.js +42 -0
- package/photon-ai/provider/ai-provider.types.js.map +1 -0
- package/photon-ai/provider/command-normalizer.d.ts +36 -0
- package/photon-ai/provider/command-normalizer.d.ts.map +1 -0
- package/photon-ai/provider/command-normalizer.js +120 -0
- package/photon-ai/provider/command-normalizer.js.map +1 -0
- package/photon-ai/provider/grid-context-builder.d.ts +37 -0
- package/photon-ai/provider/grid-context-builder.d.ts.map +1 -0
- package/photon-ai/provider/grid-context-builder.js +96 -0
- package/photon-ai/provider/grid-context-builder.js.map +1 -0
- package/photon-ai/provider/http-ai-provider.d.ts +42 -0
- package/photon-ai/provider/http-ai-provider.d.ts.map +1 -0
- package/photon-ai/provider/http-ai-provider.js +125 -0
- package/photon-ai/provider/http-ai-provider.js.map +1 -0
- package/photon-ai/provider/index.d.ts +30 -0
- package/photon-ai/provider/index.d.ts.map +1 -0
- package/photon-ai/provider/index.js +42 -0
- package/photon-ai/provider/index.js.map +1 -0
- package/photon-ai/provider/presets.d.ts +29 -0
- package/photon-ai/provider/presets.d.ts.map +1 -0
- package/photon-ai/provider/presets.js +110 -0
- package/photon-ai/provider/presets.js.map +1 -0
- package/photon-ai/provider/response-parsing.d.ts +22 -0
- package/photon-ai/provider/response-parsing.d.ts.map +1 -0
- package/photon-ai/provider/response-parsing.js +79 -0
- package/photon-ai/provider/response-parsing.js.map +1 -0
- package/photon-ai/provider/system-prompt.d.ts +26 -0
- package/photon-ai/provider/system-prompt.d.ts.map +1 -0
- package/photon-ai/provider/system-prompt.js +82 -0
- package/photon-ai/provider/system-prompt.js.map +1 -0
- package/photon-grid.min.js +497 -56
- package/renderer/body-renderer.d.ts.map +1 -1
- package/renderer/body-renderer.js +34 -9
- package/renderer/body-renderer.js.map +1 -1
- package/renderer/column-chooser.d.ts +82 -0
- package/renderer/column-chooser.d.ts.map +1 -0
- package/renderer/column-chooser.js +335 -0
- package/renderer/column-chooser.js.map +1 -0
- package/renderer/column-menu.d.ts.map +1 -1
- package/renderer/column-menu.js +32 -20
- package/renderer/column-menu.js.map +1 -1
- package/renderer/column-style-manager.d.ts +17 -0
- package/renderer/column-style-manager.d.ts.map +1 -1
- package/renderer/column-style-manager.js +28 -0
- package/renderer/column-style-manager.js.map +1 -1
- package/renderer/detail-row-renderer.d.ts.map +1 -1
- package/renderer/detail-row-renderer.js +8 -6
- package/renderer/detail-row-renderer.js.map +1 -1
- package/renderer/grid-renderer.d.ts +10 -0
- package/renderer/grid-renderer.d.ts.map +1 -1
- package/renderer/grid-renderer.js +45 -7
- package/renderer/grid-renderer.js.map +1 -1
- package/renderer/group-drop-zone.js +14 -14
- package/renderer/group-drop-zone.js.map +1 -1
- package/renderer/header-renderer.d.ts +65 -0
- package/renderer/header-renderer.d.ts.map +1 -1
- package/renderer/header-renderer.js +347 -49
- package/renderer/header-renderer.js.map +1 -1
- package/renderer/row-animator.js +3 -3
- package/renderer/row-drag-renderer.js +6 -6
- package/renderer/row-drag-renderer.js.map +1 -1
- package/renderer/scroll-controller.d.ts +40 -0
- package/renderer/scroll-controller.d.ts.map +1 -1
- package/renderer/scroll-controller.js +184 -0
- package/renderer/scroll-controller.js.map +1 -1
- package/styles/base/column-chooser.css.d.ts +13 -0
- package/styles/base/column-chooser.css.d.ts.map +1 -0
- package/styles/base/column-chooser.css.js +241 -0
- package/styles/base/column-chooser.css.js.map +1 -0
- package/styles/base/column-context-menu.css.d.ts +1 -1
- package/styles/base/column-context-menu.css.d.ts.map +1 -1
- package/styles/base/column-context-menu.css.js +10 -0
- package/styles/base/column-context-menu.css.js.map +1 -1
- package/styles/base/filter.css.d.ts +1 -1
- package/styles/base/filter.css.js +1 -1
- package/styles/base/header.css.d.ts +1 -1
- package/styles/base/header.css.d.ts.map +1 -1
- package/styles/base/header.css.js +19 -0
- package/styles/base/header.css.js.map +1 -1
- package/styles/base/photon-ai.css.d.ts +1 -1
- package/styles/base/photon-ai.css.d.ts.map +1 -1
- package/styles/base/photon-ai.css.js +65 -0
- package/styles/base/photon-ai.css.js.map +1 -1
- package/styles/base/scrollbars.css.d.ts +1 -1
- package/styles/base/scrollbars.css.d.ts.map +1 -1
- package/styles/base/scrollbars.css.js +50 -4
- package/styles/base/scrollbars.css.js.map +1 -1
- package/styles/base/touch.css.d.ts +23 -0
- package/styles/base/touch.css.d.ts.map +1 -0
- package/styles/base/touch.css.js +85 -0
- package/styles/base/touch.css.js.map +1 -0
- package/styles/base-styles.d.ts.map +1 -1
- package/styles/base-styles.js +4 -0
- package/styles/base-styles.js.map +1 -1
- package/styles/themes/theme-dark.d.ts +1 -1
- package/styles/themes/theme-dark.d.ts.map +1 -1
- package/styles/themes/theme-dark.js +3 -0
- package/styles/themes/theme-dark.js.map +1 -1
- package/types/column.types.d.ts +36 -0
- package/types/column.types.d.ts.map +1 -1
- package/types/grid.types.d.ts +3 -2
- package/types/grid.types.d.ts.map +1 -1
- package/types/photon-ai.types.d.ts +125 -5
- package/types/photon-ai.types.d.ts.map +1 -1
- package/types/photon-ai.types.js +18 -1
- 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) =>
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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('
|
|
427
|
-
document.removeEventListener('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
729
|
-
|
|
810
|
+
th.classList.remove('pg-th--drag-armed');
|
|
811
|
+
document.removeEventListener('pointermove', onMoveCheck);
|
|
812
|
+
document.removeEventListener('pointerup', onUpEarly);
|
|
730
813
|
};
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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,17 +1401,12 @@ export class HeaderRenderer {
|
|
|
1279
1401
|
this.draggingPanelRowEl = null;
|
|
1280
1402
|
document.body.style.cursor = '';
|
|
1281
1403
|
document.body.style.userSelect = '';
|
|
1282
|
-
document.removeEventListener('
|
|
1283
|
-
document.removeEventListener('
|
|
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;
|
|
1287
1409
|
const startWidth = this.colStyles.getWidth(col.colId);
|
|
1288
|
-
// Lock every flex column to its current pixel width up front, so dragging
|
|
1289
|
-
// this one resizes only it — the others keep their widths (and the total
|
|
1290
|
-
// may overflow into a horizontal scrollbar) instead of flex redistributing
|
|
1291
|
-
// the container and collapsing them to minWidth.
|
|
1292
|
-
this.colStyles.freezeFlexWidths();
|
|
1293
1410
|
// Right-pinned columns anchor their right edge to the grid border, so their
|
|
1294
1411
|
// resize handle sits on the *left* (inner) edge — the only edge free to move.
|
|
1295
1412
|
// Dragging that edge left must therefore widen the column, so the pointer
|
|
@@ -1297,11 +1414,27 @@ export class HeaderRenderer {
|
|
|
1297
1414
|
// their right-edge handle: dragging right widens.
|
|
1298
1415
|
const dir = col.pinned === 'right' ? -1 : 1;
|
|
1299
1416
|
const minWidth = col.minWidth ?? 60;
|
|
1300
|
-
thEl.classList.add('pg-th--resizing');
|
|
1301
1417
|
document.body.style.cursor = 'col-resize';
|
|
1302
1418
|
document.body.style.userSelect = 'none';
|
|
1303
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;
|
|
1304
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
|
+
}
|
|
1305
1438
|
this.colStyles.setWidth(col.colId, Math.max(minWidth, startWidth + (ev.clientX - startX) * dir));
|
|
1306
1439
|
this.onResizeCb?.();
|
|
1307
1440
|
};
|
|
@@ -1309,17 +1442,19 @@ export class HeaderRenderer {
|
|
|
1309
1442
|
// Clear the flag BEFORE setColumnWidth fires an event so the final
|
|
1310
1443
|
// post-resize render calls renderRows normally and the body is correct.
|
|
1311
1444
|
this._isResizingColumn = false;
|
|
1312
|
-
const newWidth = Math.max(minWidth, startWidth + (ev.clientX - startX) * dir);
|
|
1313
|
-
this.colStyles.setWidth(col.colId, newWidth);
|
|
1314
|
-
this.columnModel.setColumnWidth(col.colId, newWidth, true);
|
|
1315
1445
|
thEl.classList.remove('pg-th--resizing');
|
|
1316
1446
|
document.body.style.cursor = '';
|
|
1317
1447
|
document.body.style.userSelect = '';
|
|
1318
|
-
document.removeEventListener('
|
|
1319
|
-
document.removeEventListener('
|
|
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);
|
|
1320
1455
|
};
|
|
1321
|
-
document.addEventListener('
|
|
1322
|
-
document.addEventListener('
|
|
1456
|
+
document.addEventListener('pointermove', onMove);
|
|
1457
|
+
document.addEventListener('pointerup', onUp);
|
|
1323
1458
|
}
|
|
1324
1459
|
/**
|
|
1325
1460
|
* Returns `true` when `colId` (at its new position `newIdx` in the visible
|
|
@@ -1343,6 +1478,169 @@ export class HeaderRenderer {
|
|
|
1343
1478
|
const right = cols[newIdx + 1];
|
|
1344
1479
|
return !(left && siblings.has(left.colId)) && !(right && siblings.has(right.colId));
|
|
1345
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
|
+
}
|
|
1346
1644
|
onAction(action, colId) {
|
|
1347
1645
|
this.eventBus.emit(GridEventType.COLUMNS_STATE_CHANGED, {
|
|
1348
1646
|
action, colId, states: this.columnModel.getColumnStates(),
|