@alaarab/ogrid-angular 2.0.19 → 2.0.22
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/esm/components/base-datagrid-table.component.js +105 -18
- package/dist/esm/components/base-popover-cell-editor.component.js +115 -0
- package/dist/esm/components/empty-state.component.js +2 -1
- package/dist/esm/components/grid-context-menu.component.js +2 -3
- package/dist/esm/components/inline-cell-editor-template.js +107 -0
- package/dist/esm/components/marching-ants-overlay.component.js +2 -3
- package/dist/esm/components/ogrid-layout.component.js +3 -3
- package/dist/esm/components/sidebar.component.js +2 -1
- package/dist/esm/components/status-bar.component.js +2 -3
- package/dist/esm/index.js +4 -0
- package/dist/esm/services/datagrid-state.service.js +57 -26
- package/dist/esm/services/ogrid.service.js +40 -6
- package/dist/esm/styles/ogrid-theme-vars.js +53 -0
- package/dist/types/components/base-datagrid-table.component.d.ts +40 -1
- package/dist/types/components/base-popover-cell-editor.component.d.ts +33 -0
- package/dist/types/components/inline-cell-editor-template.d.ts +6 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/services/datagrid-state.service.d.ts +9 -1
- package/dist/types/services/ogrid.service.d.ts +8 -0
- package/dist/types/styles/ogrid-theme-vars.d.ts +5 -0
- package/dist/types/types/dataGridTypes.d.ts +11 -2
- package/package.json +2 -2
|
@@ -20,6 +20,8 @@ export class BaseDataGridTableComponent {
|
|
|
20
20
|
this.virtualScrollService = new VirtualScrollService();
|
|
21
21
|
this.lastMouseShift = false;
|
|
22
22
|
this.columnSizingVersion = signal(0);
|
|
23
|
+
/** Dirty flag — set when column layout changes, cleared after measurement. */
|
|
24
|
+
this.measureDirty = true;
|
|
23
25
|
/** DOM-measured column widths from the last layout pass.
|
|
24
26
|
* Used as a minWidth floor to prevent columns from shrinking
|
|
25
27
|
* when new data loads (e.g. server-side pagination). */
|
|
@@ -38,6 +40,10 @@ export class BaseDataGridTableComponent {
|
|
|
38
40
|
this.isLoading = computed(() => this.getProps()?.isLoading ?? false);
|
|
39
41
|
this.loadingMessage = computed(() => 'Loading\u2026');
|
|
40
42
|
this.layoutModeFit = computed(() => (this.getProps()?.layoutMode ?? 'fill') === 'content');
|
|
43
|
+
this.rowHeightCssVar = computed(() => {
|
|
44
|
+
const rh = this.getProps()?.rowHeight;
|
|
45
|
+
return rh ? `${rh}px` : null;
|
|
46
|
+
});
|
|
41
47
|
this.ariaLabel = computed(() => this.getProps()?.['aria-label'] ?? 'Data grid');
|
|
42
48
|
this.ariaLabelledBy = computed(() => this.getProps()?.['aria-labelledby']);
|
|
43
49
|
this.emptyState = computed(() => this.getProps()?.emptyState);
|
|
@@ -152,34 +158,44 @@ export class BaseDataGridTableComponent {
|
|
|
152
158
|
};
|
|
153
159
|
});
|
|
154
160
|
});
|
|
155
|
-
// Compute sticky offsets for pinned columns (
|
|
161
|
+
// Compute sticky offsets for pinned columns (single pass from both ends)
|
|
156
162
|
this.pinningOffsets = computed(() => {
|
|
157
163
|
const layouts = this.columnLayouts();
|
|
158
164
|
const leftOffsets = {};
|
|
159
165
|
const rightOffsets = {};
|
|
160
|
-
// Left offsets: start after checkbox and row number columns
|
|
161
166
|
let leftAcc = 0;
|
|
162
167
|
if (this.hasCheckboxCol())
|
|
163
168
|
leftAcc += CHECKBOX_COLUMN_WIDTH;
|
|
164
169
|
if (this.hasRowNumbersCol())
|
|
165
170
|
leftAcc += ROW_NUMBER_COLUMN_WIDTH;
|
|
166
|
-
for (const layout of layouts) {
|
|
167
|
-
if (layout.pinnedLeft) {
|
|
168
|
-
leftOffsets[layout.col.columnId] = leftAcc;
|
|
169
|
-
leftAcc += layout.width + CELL_PADDING;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Right offsets: walk from the end
|
|
173
171
|
let rightAcc = 0;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
const len = layouts.length;
|
|
173
|
+
for (let i = 0; i < len; i++) {
|
|
174
|
+
// Left-pinned: walk forward
|
|
175
|
+
const leftLayout = layouts[i];
|
|
176
|
+
if (leftLayout.pinnedLeft) {
|
|
177
|
+
leftOffsets[leftLayout.col.columnId] = leftAcc;
|
|
178
|
+
leftAcc += leftLayout.width + CELL_PADDING;
|
|
179
|
+
}
|
|
180
|
+
// Right-pinned: walk backward
|
|
181
|
+
const ri = len - 1 - i;
|
|
182
|
+
const rightLayout = layouts[ri];
|
|
183
|
+
if (rightLayout.pinnedRight) {
|
|
184
|
+
rightOffsets[rightLayout.col.columnId] = rightAcc;
|
|
185
|
+
rightAcc += rightLayout.width + CELL_PADDING;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
return { leftOffsets, rightOffsets };
|
|
182
189
|
});
|
|
190
|
+
/** Memoized column menu handlers — avoids recreating objects on every CD cycle */
|
|
191
|
+
this.columnMenuHandlersMap = computed(() => {
|
|
192
|
+
const cols = this.visibleCols();
|
|
193
|
+
const map = new Map();
|
|
194
|
+
for (const col of cols) {
|
|
195
|
+
map.set(col.columnId, this.buildColumnMenuHandlers(col.columnId));
|
|
196
|
+
}
|
|
197
|
+
return map;
|
|
198
|
+
});
|
|
183
199
|
}
|
|
184
200
|
/** Lifecycle hook — populate element signals from @ViewChild refs */
|
|
185
201
|
ngAfterViewInit() {
|
|
@@ -191,9 +207,12 @@ export class BaseDataGridTableComponent {
|
|
|
191
207
|
this.tableContainerElSignal.set(tableContainer);
|
|
192
208
|
this.measureColumnWidths();
|
|
193
209
|
}
|
|
194
|
-
/** Lifecycle hook — re-measure column widths
|
|
210
|
+
/** Lifecycle hook — re-measure column widths only when layout changed */
|
|
195
211
|
ngAfterViewChecked() {
|
|
196
|
-
this.
|
|
212
|
+
if (this.measureDirty) {
|
|
213
|
+
this.measureDirty = false;
|
|
214
|
+
this.measureColumnWidths();
|
|
215
|
+
}
|
|
197
216
|
}
|
|
198
217
|
/** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
|
|
199
218
|
* Only updates the signal when values actually change, to avoid render loops. */
|
|
@@ -255,6 +274,14 @@ export class BaseDataGridTableComponent {
|
|
|
255
274
|
this.columnReorderService.enabled.set(p.columnReorder === true);
|
|
256
275
|
}
|
|
257
276
|
});
|
|
277
|
+
// Mark measurement dirty when column layout changes
|
|
278
|
+
effect(() => {
|
|
279
|
+
// Track signals that affect column layout
|
|
280
|
+
this.visibleCols();
|
|
281
|
+
this.columnSizingOverrides();
|
|
282
|
+
this.columnSizingVersion();
|
|
283
|
+
this.measureDirty = true;
|
|
284
|
+
});
|
|
258
285
|
// Wire virtual scroll service inputs
|
|
259
286
|
effect(() => {
|
|
260
287
|
const p = this.getProps();
|
|
@@ -314,6 +341,24 @@ export class BaseDataGridTableComponent {
|
|
|
314
341
|
getFilterConfig(col) {
|
|
315
342
|
return getHeaderFilterConfig(col, this.headerFilterInput());
|
|
316
343
|
}
|
|
344
|
+
/** Build column menu handler object for a single column */
|
|
345
|
+
buildColumnMenuHandlers(columnId) {
|
|
346
|
+
return {
|
|
347
|
+
onPinLeft: () => this.onPinColumn(columnId, 'left'),
|
|
348
|
+
onPinRight: () => this.onPinColumn(columnId, 'right'),
|
|
349
|
+
onUnpin: () => this.onUnpinColumn(columnId),
|
|
350
|
+
onSortAsc: () => this.onSortAsc(columnId),
|
|
351
|
+
onSortDesc: () => this.onSortDesc(columnId),
|
|
352
|
+
onClearSort: () => this.onClearSort(columnId),
|
|
353
|
+
onAutosizeThis: () => this.onAutosizeColumn(columnId),
|
|
354
|
+
onAutosizeAll: () => this.onAutosizeAllColumns(),
|
|
355
|
+
onClose: () => { }
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/** Get memoized handlers for a column */
|
|
359
|
+
getColumnMenuHandlersMemoized(columnId) {
|
|
360
|
+
return this.columnMenuHandlersMap().get(columnId) ?? this.buildColumnMenuHandlers(columnId);
|
|
361
|
+
}
|
|
317
362
|
getCellDescriptor(item, col, rowIndex, colIdx) {
|
|
318
363
|
return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
|
|
319
364
|
}
|
|
@@ -330,6 +375,48 @@ export class BaseDataGridTableComponent {
|
|
|
330
375
|
cancelPopoverEdit: () => this.cancelPopoverEdit(),
|
|
331
376
|
});
|
|
332
377
|
}
|
|
378
|
+
/** Check if a specific cell is the active cell (PrimeNG inline template helper). */
|
|
379
|
+
isActiveCell(rowIndex, colIdx) {
|
|
380
|
+
const ac = this.activeCell();
|
|
381
|
+
if (!ac)
|
|
382
|
+
return false;
|
|
383
|
+
return ac.rowIndex === rowIndex && ac.columnIndex === colIdx + this.colOffset();
|
|
384
|
+
}
|
|
385
|
+
/** Check if a cell is within the current selection range (PrimeNG inline template helper). */
|
|
386
|
+
isInSelectionRange(rowIndex, colIdx) {
|
|
387
|
+
const range = this.selectionRange();
|
|
388
|
+
if (!range)
|
|
389
|
+
return false;
|
|
390
|
+
const minR = Math.min(range.startRow, range.endRow);
|
|
391
|
+
const maxR = Math.max(range.startRow, range.endRow);
|
|
392
|
+
const minC = Math.min(range.startCol, range.endCol);
|
|
393
|
+
const maxC = Math.max(range.startCol, range.endCol);
|
|
394
|
+
return rowIndex >= minR && rowIndex <= maxR && colIdx >= minC && colIdx <= maxC;
|
|
395
|
+
}
|
|
396
|
+
/** Check if a cell is the selection end cell for fill handle display. */
|
|
397
|
+
isSelectionEndCell(rowIndex, colIdx) {
|
|
398
|
+
const range = this.selectionRange();
|
|
399
|
+
if (!range || this.isDragging() || this.copyRange() || this.cutRange())
|
|
400
|
+
return false;
|
|
401
|
+
return rowIndex === range.endRow && colIdx === range.endCol;
|
|
402
|
+
}
|
|
403
|
+
/** Get cell background color based on selection state. */
|
|
404
|
+
getCellBackground(rowIndex, colIdx) {
|
|
405
|
+
if (this.isInSelectionRange(rowIndex, colIdx))
|
|
406
|
+
return 'var(--ogrid-range-bg, rgba(33, 115, 70, 0.08))';
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
/** Resolve editor type from column definition. */
|
|
410
|
+
getEditorType(col, _item) {
|
|
411
|
+
if (col.cellEditor === 'text' || col.cellEditor === 'select' || col.cellEditor === 'checkbox' || col.cellEditor === 'date' || col.cellEditor === 'richSelect') {
|
|
412
|
+
return col.cellEditor;
|
|
413
|
+
}
|
|
414
|
+
if (col.type === 'date')
|
|
415
|
+
return 'date';
|
|
416
|
+
if (col.type === 'boolean')
|
|
417
|
+
return 'checkbox';
|
|
418
|
+
return 'text';
|
|
419
|
+
}
|
|
333
420
|
getSelectValues(col) {
|
|
334
421
|
const params = col.cellEditorParams;
|
|
335
422
|
if (params && typeof params === 'object' && 'values' in params) {
|
|
@@ -524,7 +611,7 @@ export class BaseDataGridTableComponent {
|
|
|
524
611
|
...this.columnSizingOverrides(),
|
|
525
612
|
[columnId]: { widthPx: width },
|
|
526
613
|
});
|
|
527
|
-
this.state().layout.onColumnResized?.(columnId, width);
|
|
614
|
+
(this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(columnId, width);
|
|
528
615
|
}
|
|
529
616
|
onAutosizeAllColumns() {
|
|
530
617
|
const tableEl = this.tableContainerEl() ?? undefined;
|
|
@@ -532,7 +619,7 @@ export class BaseDataGridTableComponent {
|
|
|
532
619
|
for (const col of this.visibleCols()) {
|
|
533
620
|
const width = measureColumnContentWidth(col.columnId, col.minWidth, tableEl);
|
|
534
621
|
overrides[col.columnId] = { widthPx: width };
|
|
535
|
-
this.state().layout.onColumnResized?.(col.columnId, width);
|
|
622
|
+
(this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(col.columnId, width);
|
|
536
623
|
}
|
|
537
624
|
this.state().layout.setColumnSizingOverrides({
|
|
538
625
|
...this.columnSizingOverrides(),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Input, ViewChild, Injector, EnvironmentInjector, inject, signal, effect, createComponent } from '@angular/core';
|
|
8
|
+
/**
|
|
9
|
+
* Shared popover cell editor template used by all Angular UI packages.
|
|
10
|
+
*/
|
|
11
|
+
export const POPOVER_CELL_EDITOR_TEMPLATE = `
|
|
12
|
+
<div #anchorEl
|
|
13
|
+
class="ogrid-popover-anchor"
|
|
14
|
+
[attr.data-row-index]="rowIndex"
|
|
15
|
+
[attr.data-col-index]="globalColIndex"
|
|
16
|
+
>
|
|
17
|
+
{{ displayValue }}
|
|
18
|
+
</div>
|
|
19
|
+
@if (showEditor()) {
|
|
20
|
+
<div class="ogrid-popover-editor-overlay" (click)="handleOverlayClick()">
|
|
21
|
+
<div class="ogrid-popover-editor-content" #editorContainer></div>
|
|
22
|
+
</div>
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
/**
|
|
26
|
+
* Shared overlay + content styles for popover cell editors.
|
|
27
|
+
* Subclasses provide their own .ogrid-popover-anchor styles.
|
|
28
|
+
*/
|
|
29
|
+
export const POPOVER_CELL_EDITOR_OVERLAY_STYLES = `
|
|
30
|
+
:host { display: contents; }
|
|
31
|
+
.ogrid-popover-editor-overlay {
|
|
32
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
33
|
+
background: rgba(0,0,0,0.3);
|
|
34
|
+
display: flex; align-items: center; justify-content: center;
|
|
35
|
+
}
|
|
36
|
+
.ogrid-popover-editor-content {
|
|
37
|
+
background: var(--ogrid-bg, #ffffff); border-radius: 4px; padding: 16px;
|
|
38
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
39
|
+
max-width: 90vw; max-height: 90vh; overflow: auto;
|
|
40
|
+
color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
/**
|
|
44
|
+
* Abstract base class for Angular popover cell editors.
|
|
45
|
+
* Contains all shared inputs, ViewChild refs, effects, and overlay click handling.
|
|
46
|
+
*
|
|
47
|
+
* Subclasses only need a @Component decorator with selector, template, and
|
|
48
|
+
* framework-specific .ogrid-popover-anchor CSS styles.
|
|
49
|
+
*/
|
|
50
|
+
export class BasePopoverCellEditorComponent {
|
|
51
|
+
constructor() {
|
|
52
|
+
this.injector = inject(Injector);
|
|
53
|
+
this.envInjector = inject(EnvironmentInjector);
|
|
54
|
+
this.showEditor = signal(false);
|
|
55
|
+
// Show editor after anchor is rendered
|
|
56
|
+
effect(() => {
|
|
57
|
+
const anchor = this.anchorRef;
|
|
58
|
+
if (anchor) {
|
|
59
|
+
setTimeout(() => this.showEditor.set(true), 0);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Render custom editor component when container is available
|
|
63
|
+
effect(() => {
|
|
64
|
+
const container = this.editorContainerRef;
|
|
65
|
+
const props = this.editorProps;
|
|
66
|
+
const col = this.column;
|
|
67
|
+
if (!container || !this.showEditor() || typeof col.cellEditor !== 'function')
|
|
68
|
+
return;
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
const EditorComponent = col.cellEditor; // ComponentType
|
|
71
|
+
const componentRef = createComponent(EditorComponent, {
|
|
72
|
+
environmentInjector: this.envInjector,
|
|
73
|
+
elementInjector: this.injector,
|
|
74
|
+
});
|
|
75
|
+
// Pass props to component instance
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
Object.assign(componentRef.instance, props);
|
|
78
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
79
|
+
// Append to DOM
|
|
80
|
+
container.nativeElement.appendChild(componentRef.location.nativeElement);
|
|
81
|
+
// Cleanup on destroy
|
|
82
|
+
return () => componentRef.destroy();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
handleOverlayClick() {
|
|
86
|
+
this.onCancel();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
__decorate([
|
|
90
|
+
Input({ required: true })
|
|
91
|
+
], BasePopoverCellEditorComponent.prototype, "item", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
Input({ required: true })
|
|
94
|
+
], BasePopoverCellEditorComponent.prototype, "column", void 0);
|
|
95
|
+
__decorate([
|
|
96
|
+
Input({ required: true })
|
|
97
|
+
], BasePopoverCellEditorComponent.prototype, "rowIndex", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
Input({ required: true })
|
|
100
|
+
], BasePopoverCellEditorComponent.prototype, "globalColIndex", void 0);
|
|
101
|
+
__decorate([
|
|
102
|
+
Input({ required: true })
|
|
103
|
+
], BasePopoverCellEditorComponent.prototype, "displayValue", void 0);
|
|
104
|
+
__decorate([
|
|
105
|
+
Input({ required: true })
|
|
106
|
+
], BasePopoverCellEditorComponent.prototype, "editorProps", void 0);
|
|
107
|
+
__decorate([
|
|
108
|
+
Input({ required: true })
|
|
109
|
+
], BasePopoverCellEditorComponent.prototype, "onCancel", void 0);
|
|
110
|
+
__decorate([
|
|
111
|
+
ViewChild('anchorEl')
|
|
112
|
+
], BasePopoverCellEditorComponent.prototype, "anchorRef", void 0);
|
|
113
|
+
__decorate([
|
|
114
|
+
ViewChild('editorContainer')
|
|
115
|
+
], BasePopoverCellEditorComponent.prototype, "editorContainerRef", void 0);
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
7
|
+
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
|
8
8
|
import { CommonModule } from '@angular/common';
|
|
9
9
|
let EmptyStateComponent = class EmptyStateComponent {
|
|
10
10
|
constructor() {
|
|
@@ -30,6 +30,7 @@ EmptyStateComponent = __decorate([
|
|
|
30
30
|
Component({
|
|
31
31
|
selector: 'ogrid-empty-state',
|
|
32
32
|
standalone: true,
|
|
33
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
34
|
imports: [CommonModule],
|
|
34
35
|
styles: [`
|
|
35
36
|
.ogrid-empty-state-clear-btn {
|
|
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input, Output, EventEmitter, ViewChild, DestroyRef, inject } from '@angular/core';
|
|
8
|
-
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { Component, Input, Output, EventEmitter, ViewChild, DestroyRef, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
9
8
|
import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
|
|
10
9
|
let GridContextMenuComponent = class GridContextMenuComponent {
|
|
11
10
|
constructor() {
|
|
@@ -119,7 +118,7 @@ GridContextMenuComponent = __decorate([
|
|
|
119
118
|
Component({
|
|
120
119
|
selector: 'ogrid-context-menu',
|
|
121
120
|
standalone: true,
|
|
122
|
-
|
|
121
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
123
122
|
template: `
|
|
124
123
|
<div
|
|
125
124
|
#menuRef
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared inline cell editor template used by all Angular UI packages.
|
|
3
|
+
* The template is identical across Material, PrimeNG, and Radix implementations.
|
|
4
|
+
*/
|
|
5
|
+
export const INLINE_CELL_EDITOR_TEMPLATE = `
|
|
6
|
+
@switch (editorType) {
|
|
7
|
+
@case ('text') {
|
|
8
|
+
<input
|
|
9
|
+
#inputEl
|
|
10
|
+
type="text"
|
|
11
|
+
[value]="localValue()"
|
|
12
|
+
(input)="localValue.set($any($event.target).value)"
|
|
13
|
+
(keydown)="onTextKeyDown($event)"
|
|
14
|
+
(blur)="onTextBlur()"
|
|
15
|
+
[style]="getInputStyle()"
|
|
16
|
+
/>
|
|
17
|
+
}
|
|
18
|
+
@case ('richSelect') {
|
|
19
|
+
<div #richSelectWrapper
|
|
20
|
+
style="width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative">
|
|
21
|
+
<input
|
|
22
|
+
#richSelectInput
|
|
23
|
+
type="text"
|
|
24
|
+
[value]="searchText()"
|
|
25
|
+
(input)="onRichSelectSearch($any($event.target).value)"
|
|
26
|
+
(keydown)="onRichSelectKeyDown($event)"
|
|
27
|
+
placeholder="Search..."
|
|
28
|
+
style="width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0"
|
|
29
|
+
/>
|
|
30
|
+
<div #richSelectDropdown role="listbox"
|
|
31
|
+
style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
|
|
32
|
+
@for (opt of filteredOptions(); track opt; let i = $index) {
|
|
33
|
+
<div role="option"
|
|
34
|
+
[attr.aria-selected]="i === highlightedIndex()"
|
|
35
|
+
(click)="commitValue(opt)"
|
|
36
|
+
[style]="i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'">
|
|
37
|
+
{{ getDisplayText(opt) }}
|
|
38
|
+
</div>
|
|
39
|
+
}
|
|
40
|
+
@if (filteredOptions().length === 0) {
|
|
41
|
+
<div style="padding:6px 8px;color:var(--ogrid-muted, #999)">No matches</div>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
}
|
|
46
|
+
@case ('select') {
|
|
47
|
+
<div #selectWrapper tabindex="0"
|
|
48
|
+
style="width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative"
|
|
49
|
+
(keydown)="onCustomSelectKeyDown($event)">
|
|
50
|
+
<div style="display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit">
|
|
51
|
+
<span>{{ getDisplayText(value) }}</span>
|
|
52
|
+
<span style="margin-left:4px;font-size:10px;opacity:0.5">▾</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div #selectDropdown role="listbox"
|
|
55
|
+
style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
|
|
56
|
+
@for (opt of selectOptions(); track opt; let i = $index) {
|
|
57
|
+
<div role="option"
|
|
58
|
+
[attr.aria-selected]="i === highlightedIndex()"
|
|
59
|
+
(click)="commitValue(opt)"
|
|
60
|
+
[style]="i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'">
|
|
61
|
+
{{ getDisplayText(opt) }}
|
|
62
|
+
</div>
|
|
63
|
+
}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
}
|
|
67
|
+
@case ('checkbox') {
|
|
68
|
+
<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%">
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
[checked]="!!localValue()"
|
|
72
|
+
(change)="commitValue($any($event.target).checked)"
|
|
73
|
+
(keydown)="onCheckboxKeyDown($event)"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
}
|
|
77
|
+
@case ('date') {
|
|
78
|
+
<input
|
|
79
|
+
#inputEl
|
|
80
|
+
type="date"
|
|
81
|
+
[value]="localValue()"
|
|
82
|
+
(change)="commitValue($any($event.target).value)"
|
|
83
|
+
(keydown)="onTextKeyDown($event)"
|
|
84
|
+
(blur)="onTextBlur()"
|
|
85
|
+
[style]="getInputStyle()"
|
|
86
|
+
/>
|
|
87
|
+
}
|
|
88
|
+
@default {
|
|
89
|
+
<input
|
|
90
|
+
#inputEl
|
|
91
|
+
type="text"
|
|
92
|
+
[value]="localValue()"
|
|
93
|
+
(input)="localValue.set($any($event.target).value)"
|
|
94
|
+
(keydown)="onTextKeyDown($event)"
|
|
95
|
+
(blur)="onTextBlur()"
|
|
96
|
+
[style]="getInputStyle()"
|
|
97
|
+
/>
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
export const INLINE_CELL_EDITOR_STYLES = `
|
|
102
|
+
:host {
|
|
103
|
+
display: block;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: 100%;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input, signal, DestroyRef, inject } from '@angular/core';
|
|
8
|
-
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { Component, Input, signal, DestroyRef, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
9
8
|
import { measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
|
|
10
9
|
let MarchingAntsOverlayComponent = class MarchingAntsOverlayComponent {
|
|
11
10
|
constructor() {
|
|
@@ -112,7 +111,7 @@ MarchingAntsOverlayComponent = __decorate([
|
|
|
112
111
|
Component({
|
|
113
112
|
selector: 'ogrid-marching-ants-overlay',
|
|
114
113
|
standalone: true,
|
|
115
|
-
|
|
114
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
116
115
|
styles: [`
|
|
117
116
|
.ogrid-marching-ants-svg { position: absolute; pointer-events: none; overflow: visible; }
|
|
118
117
|
.ogrid-marching-ants-svg--selection { z-index: 4; }
|
|
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
|
8
|
-
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { Component, Input, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core';
|
|
9
8
|
import { SideBarComponent } from './sidebar.component';
|
|
10
9
|
import { GRID_BORDER_RADIUS } from '@alaarab/ogrid-core';
|
|
11
10
|
let OGridLayoutComponent = class OGridLayoutComponent {
|
|
@@ -37,7 +36,8 @@ OGridLayoutComponent = __decorate([
|
|
|
37
36
|
selector: 'ogrid-layout',
|
|
38
37
|
standalone: true,
|
|
39
38
|
encapsulation: ViewEncapsulation.None,
|
|
40
|
-
|
|
39
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
40
|
+
imports: [SideBarComponent],
|
|
41
41
|
styles: [`
|
|
42
42
|
/* ─── OGrid Theme Variables ─── */
|
|
43
43
|
:root {
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input } from '@angular/core';
|
|
7
|
+
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
|
8
8
|
import { CommonModule } from '@angular/common';
|
|
9
9
|
const PANEL_WIDTH = 240;
|
|
10
10
|
const TAB_WIDTH = 36;
|
|
@@ -100,6 +100,7 @@ SideBarComponent = __decorate([
|
|
|
100
100
|
Component({
|
|
101
101
|
selector: 'ogrid-sidebar',
|
|
102
102
|
standalone: true,
|
|
103
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
103
104
|
imports: [CommonModule],
|
|
104
105
|
styles: [`
|
|
105
106
|
.ogrid-sidebar-root { display: flex; flex-direction: row; flex-shrink: 0; }
|
|
@@ -4,8 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component, Input } from '@angular/core';
|
|
8
|
-
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
|
9
8
|
import { getStatusBarParts } from '@alaarab/ogrid-core';
|
|
10
9
|
let StatusBarComponent = class StatusBarComponent {
|
|
11
10
|
constructor() {
|
|
@@ -52,7 +51,7 @@ StatusBarComponent = __decorate([
|
|
|
52
51
|
Component({
|
|
53
52
|
selector: 'ogrid-status-bar',
|
|
54
53
|
standalone: true,
|
|
55
|
-
|
|
54
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
56
55
|
template: `
|
|
57
56
|
<div [class]="classNames?.statusBar ?? ''" role="status" aria-live="polite">
|
|
58
57
|
@for (part of getParts(); track part.key) {
|
package/dist/esm/index.js
CHANGED
|
@@ -20,4 +20,8 @@ export { BaseColumnHeaderFilterComponent } from './components/base-column-header
|
|
|
20
20
|
export { BaseColumnChooserComponent } from './components/base-column-chooser.component';
|
|
21
21
|
export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
|
|
22
22
|
export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
|
|
23
|
+
export { INLINE_CELL_EDITOR_TEMPLATE, INLINE_CELL_EDITOR_STYLES } from './components/inline-cell-editor-template';
|
|
24
|
+
export { BasePopoverCellEditorComponent, POPOVER_CELL_EDITOR_TEMPLATE, POPOVER_CELL_EDITOR_OVERLAY_STYLES } from './components/base-popover-cell-editor.component';
|
|
25
|
+
// Shared styles
|
|
26
|
+
export { OGRID_THEME_VARS_CSS } from './styles/ogrid-theme-vars';
|
|
23
27
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Injectable, signal, computed, effect, DestroyRef, inject } from '@angular/core';
|
|
7
|
+
import { Injectable, signal, computed, effect, DestroyRef, inject, NgZone } from '@angular/core';
|
|
8
8
|
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, UndoRedoStack, findCtrlArrowTarget, computeTabNavigation, formatSelectionAsTsv, parseTsvClipboard, rangesEqual, } from '@alaarab/ogrid-core';
|
|
9
9
|
/**
|
|
10
10
|
* Single orchestration service for DataGridTable. Takes grid props,
|
|
@@ -15,6 +15,7 @@ import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregat
|
|
|
15
15
|
let DataGridStateService = class DataGridStateService {
|
|
16
16
|
constructor() {
|
|
17
17
|
this.destroyRef = inject(DestroyRef);
|
|
18
|
+
this.ngZone = inject(NgZone);
|
|
18
19
|
// --- Input signals ---
|
|
19
20
|
this.props = signal(null);
|
|
20
21
|
this.wrapperEl = signal(null);
|
|
@@ -39,6 +40,9 @@ let DataGridStateService = class DataGridStateService {
|
|
|
39
40
|
this.redoLengthSig = signal(0);
|
|
40
41
|
// Fill handle state
|
|
41
42
|
this.fillDragStart = null;
|
|
43
|
+
this.fillRafId = 0;
|
|
44
|
+
this.fillMoveHandler = null;
|
|
45
|
+
this.fillUpHandler = null;
|
|
42
46
|
// Row selection
|
|
43
47
|
this.lastClickedRow = -1;
|
|
44
48
|
// Drag selection refs
|
|
@@ -61,10 +65,12 @@ let DataGridStateService = class DataGridStateService {
|
|
|
61
65
|
const p = this.props();
|
|
62
66
|
return p ? p.cellSelection !== false : true;
|
|
63
67
|
});
|
|
64
|
-
//
|
|
68
|
+
// Narrow signal extractors — prevent full props() dependency in effects/computed
|
|
69
|
+
this.originalOnCellValueChanged = computed(() => this.props()?.onCellValueChanged);
|
|
70
|
+
this.initialColumnWidthsSig = computed(() => this.props()?.initialColumnWidths);
|
|
71
|
+
// Undo/redo wrapped callback — only recomputes when the actual callback reference changes
|
|
65
72
|
this.wrappedOnCellValueChanged = computed(() => {
|
|
66
|
-
const
|
|
67
|
-
const original = p?.onCellValueChanged;
|
|
73
|
+
const original = this.originalOnCellValueChanged();
|
|
68
74
|
if (!original)
|
|
69
75
|
return undefined;
|
|
70
76
|
return (event) => {
|
|
@@ -202,29 +208,33 @@ let DataGridStateService = class DataGridStateService {
|
|
|
202
208
|
return p.items.length === 0 && !!p.emptyState && !p.isLoading;
|
|
203
209
|
});
|
|
204
210
|
// Setup window event listeners for cell selection drag
|
|
205
|
-
//
|
|
211
|
+
// Run outside NgZone to avoid 60Hz change detection during drag
|
|
206
212
|
effect((onCleanup) => {
|
|
207
213
|
const onMove = (e) => this.onWindowMouseMove(e);
|
|
208
214
|
const onUp = () => this.onWindowMouseUp();
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
this.ngZone.runOutsideAngular(() => {
|
|
216
|
+
window.addEventListener('mousemove', onMove, true);
|
|
217
|
+
window.addEventListener('mouseup', onUp, true);
|
|
218
|
+
});
|
|
211
219
|
onCleanup(() => {
|
|
212
220
|
window.removeEventListener('mousemove', onMove, true);
|
|
213
221
|
window.removeEventListener('mouseup', onUp, true);
|
|
214
222
|
});
|
|
215
223
|
});
|
|
216
224
|
// Initialize column sizing overrides from initial widths
|
|
225
|
+
// Only track initialColumnWidths, not all props
|
|
217
226
|
effect(() => {
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
227
|
+
const widths = this.initialColumnWidthsSig();
|
|
228
|
+
if (widths) {
|
|
220
229
|
const result = {};
|
|
221
|
-
for (const [id, width] of Object.entries(
|
|
230
|
+
for (const [id, width] of Object.entries(widths)) {
|
|
222
231
|
result[id] = { widthPx: width };
|
|
223
232
|
}
|
|
224
233
|
this.columnSizingOverridesSig.set(result);
|
|
225
234
|
}
|
|
226
235
|
});
|
|
227
236
|
// Container width measurement via ResizeObserver
|
|
237
|
+
// Run outside NgZone — signal.set() inside still triggers Angular reactivity
|
|
228
238
|
effect(() => {
|
|
229
239
|
const el = this.wrapperEl();
|
|
230
240
|
if (this.resizeObserver) {
|
|
@@ -240,16 +250,22 @@ let DataGridStateService = class DataGridStateService {
|
|
|
240
250
|
(parseFloat(cs.borderRightWidth || '0') || 0);
|
|
241
251
|
this.containerWidthSig.set(Math.max(0, rect.width - borderX));
|
|
242
252
|
};
|
|
243
|
-
this.
|
|
244
|
-
|
|
253
|
+
this.ngZone.runOutsideAngular(() => {
|
|
254
|
+
this.resizeObserver = new ResizeObserver(measure);
|
|
255
|
+
this.resizeObserver.observe(el);
|
|
256
|
+
});
|
|
245
257
|
measure();
|
|
246
258
|
});
|
|
247
|
-
// Cleanup on destroy —
|
|
259
|
+
// Cleanup on destroy — cancel pending work and release references
|
|
248
260
|
this.destroyRef.onDestroy(() => {
|
|
249
261
|
if (this.rafId) {
|
|
250
262
|
cancelAnimationFrame(this.rafId);
|
|
251
263
|
this.rafId = 0;
|
|
252
264
|
}
|
|
265
|
+
if (this.fillRafId) {
|
|
266
|
+
cancelAnimationFrame(this.fillRafId);
|
|
267
|
+
this.fillRafId = 0;
|
|
268
|
+
}
|
|
253
269
|
if (this.autoScrollInterval) {
|
|
254
270
|
clearInterval(this.autoScrollInterval);
|
|
255
271
|
this.autoScrollInterval = null;
|
|
@@ -258,6 +274,17 @@ let DataGridStateService = class DataGridStateService {
|
|
|
258
274
|
this.resizeObserver.disconnect();
|
|
259
275
|
this.resizeObserver = null;
|
|
260
276
|
}
|
|
277
|
+
// Remove fill-handle window listeners if active
|
|
278
|
+
if (this.fillMoveHandler) {
|
|
279
|
+
window.removeEventListener('mousemove', this.fillMoveHandler, true);
|
|
280
|
+
this.fillMoveHandler = null;
|
|
281
|
+
}
|
|
282
|
+
if (this.fillUpHandler) {
|
|
283
|
+
window.removeEventListener('mouseup', this.fillUpHandler, true);
|
|
284
|
+
this.fillUpHandler = null;
|
|
285
|
+
}
|
|
286
|
+
// Clear undo/redo stack to release closure references
|
|
287
|
+
this.undoRedoStack.clear();
|
|
261
288
|
});
|
|
262
289
|
// Clean up column sizing overrides for removed columns
|
|
263
290
|
effect(() => {
|
|
@@ -956,6 +983,7 @@ let DataGridStateService = class DataGridStateService {
|
|
|
956
983
|
columnSizingOverrides: this.columnSizingOverridesSig(),
|
|
957
984
|
setColumnSizingOverrides: (overrides) => this.columnSizingOverridesSig.set(overrides),
|
|
958
985
|
onColumnResized: p?.onColumnResized,
|
|
986
|
+
onAutosizeColumn: p?.onAutosizeColumn,
|
|
959
987
|
};
|
|
960
988
|
const rowSelection = {
|
|
961
989
|
selectedRowIds: this.selectedRowIds(),
|
|
@@ -1191,7 +1219,6 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1191
1219
|
const fillStart = this.fillDragStart;
|
|
1192
1220
|
let fillDragEnd = { endRow: fillStart.startRow, endCol: fillStart.startCol };
|
|
1193
1221
|
let liveFillRange = null;
|
|
1194
|
-
let fillRafId = 0;
|
|
1195
1222
|
let lastFillMousePos = null;
|
|
1196
1223
|
const resolveRange = (cx, cy) => {
|
|
1197
1224
|
const target = document.elementFromPoint(cx, cy);
|
|
@@ -1211,10 +1238,10 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1211
1238
|
};
|
|
1212
1239
|
const onMove = (e) => {
|
|
1213
1240
|
lastFillMousePos = { cx: e.clientX, cy: e.clientY };
|
|
1214
|
-
if (fillRafId)
|
|
1215
|
-
cancelAnimationFrame(fillRafId);
|
|
1216
|
-
fillRafId = requestAnimationFrame(() => {
|
|
1217
|
-
fillRafId = 0;
|
|
1241
|
+
if (this.fillRafId)
|
|
1242
|
+
cancelAnimationFrame(this.fillRafId);
|
|
1243
|
+
this.fillRafId = requestAnimationFrame(() => {
|
|
1244
|
+
this.fillRafId = 0;
|
|
1218
1245
|
if (!lastFillMousePos)
|
|
1219
1246
|
return;
|
|
1220
1247
|
const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
@@ -1233,9 +1260,11 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1233
1260
|
const onUp = () => {
|
|
1234
1261
|
window.removeEventListener('mousemove', onMove, true);
|
|
1235
1262
|
window.removeEventListener('mouseup', onUp, true);
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1263
|
+
this.fillMoveHandler = null;
|
|
1264
|
+
this.fillUpHandler = null;
|
|
1265
|
+
if (this.fillRafId) {
|
|
1266
|
+
cancelAnimationFrame(this.fillRafId);
|
|
1267
|
+
this.fillRafId = 0;
|
|
1239
1268
|
}
|
|
1240
1269
|
if (lastFillMousePos) {
|
|
1241
1270
|
const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
@@ -1281,12 +1310,14 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1281
1310
|
this.endBatch();
|
|
1282
1311
|
}
|
|
1283
1312
|
this.fillDragStart = null;
|
|
1284
|
-
// Remove event listeners after mouseup completes
|
|
1285
|
-
window.removeEventListener('mousemove', onMove, true);
|
|
1286
|
-
window.removeEventListener('mouseup', onUp, true);
|
|
1287
1313
|
};
|
|
1288
|
-
|
|
1289
|
-
|
|
1314
|
+
// Track handlers for cleanup on destroy
|
|
1315
|
+
this.fillMoveHandler = onMove;
|
|
1316
|
+
this.fillUpHandler = onUp;
|
|
1317
|
+
this.ngZone.runOutsideAngular(() => {
|
|
1318
|
+
window.addEventListener('mousemove', onMove, true);
|
|
1319
|
+
window.addEventListener('mouseup', onUp, true);
|
|
1320
|
+
});
|
|
1290
1321
|
}
|
|
1291
1322
|
};
|
|
1292
1323
|
DataGridStateService = __decorate([
|
|
@@ -37,6 +37,7 @@ let OGridService = class OGridService {
|
|
|
37
37
|
this.columnOrder = signal(undefined);
|
|
38
38
|
this.onColumnOrderChange = signal(undefined);
|
|
39
39
|
this.onColumnResized = signal(undefined);
|
|
40
|
+
this.onAutosizeColumn = signal(undefined);
|
|
40
41
|
this.onColumnPinned = signal(undefined);
|
|
41
42
|
this.defaultPageSize = signal(DEFAULT_PAGE_SIZE);
|
|
42
43
|
this.defaultSortBy = signal(undefined);
|
|
@@ -51,6 +52,7 @@ let OGridService = class OGridService {
|
|
|
51
52
|
this.editable = signal(undefined);
|
|
52
53
|
this.cellSelection = signal(undefined);
|
|
53
54
|
this.density = signal('normal');
|
|
55
|
+
this.rowHeight = signal(undefined);
|
|
54
56
|
this.onCellValueChanged = signal(undefined);
|
|
55
57
|
this.onUndo = signal(undefined);
|
|
56
58
|
this.onRedo = signal(undefined);
|
|
@@ -207,6 +209,14 @@ let OGridService = class OGridService {
|
|
|
207
209
|
toggle: (panel) => this.sideBarActivePanel.update((p) => p === panel ? null : panel),
|
|
208
210
|
close: () => this.sideBarActivePanel.set(null),
|
|
209
211
|
}));
|
|
212
|
+
// --- Pre-computed stable callback references for dataGridProps ---
|
|
213
|
+
// These avoid recreating arrow functions on every dataGridProps recomputation.
|
|
214
|
+
this.handleSortFn = (columnKey, direction) => this.handleSort(columnKey, direction);
|
|
215
|
+
this.handleColumnResizedFn = (columnId, width) => this.handleColumnResized(columnId, width);
|
|
216
|
+
this.handleColumnPinnedFn = (columnId, pinned) => this.handleColumnPinned(columnId, pinned);
|
|
217
|
+
this.handleSelectionChangeFn = (event) => this.handleSelectionChange(event);
|
|
218
|
+
this.handleFilterChangeFn = (key, value) => this.handleFilterChange(key, value);
|
|
219
|
+
this.clearAllFiltersFn = () => this.setFilters({});
|
|
210
220
|
// --- Data grid props computed ---
|
|
211
221
|
this.dataGridProps = computed(() => ({
|
|
212
222
|
items: this.displayItems(),
|
|
@@ -214,17 +224,19 @@ let OGridService = class OGridService {
|
|
|
214
224
|
getRowId: this.getRowId(),
|
|
215
225
|
sortBy: this.sort().field,
|
|
216
226
|
sortDirection: this.sort().direction,
|
|
217
|
-
onColumnSort:
|
|
227
|
+
onColumnSort: this.handleSortFn,
|
|
218
228
|
visibleColumns: this.visibleColumns(),
|
|
219
229
|
columnOrder: this.columnOrder(),
|
|
220
230
|
onColumnOrderChange: this.onColumnOrderChange(),
|
|
221
|
-
onColumnResized:
|
|
222
|
-
|
|
231
|
+
onColumnResized: this.handleColumnResizedFn,
|
|
232
|
+
onAutosizeColumn: this.onAutosizeColumn(),
|
|
233
|
+
onColumnPinned: this.handleColumnPinnedFn,
|
|
223
234
|
pinnedColumns: this.pinnedOverrides(),
|
|
224
235
|
initialColumnWidths: this.columnWidthOverrides(),
|
|
225
236
|
editable: this.editable(),
|
|
226
237
|
cellSelection: this.cellSelection(),
|
|
227
238
|
density: this.density(),
|
|
239
|
+
rowHeight: this.rowHeight(),
|
|
228
240
|
onCellValueChanged: this.onCellValueChanged(),
|
|
229
241
|
onUndo: this.onUndo(),
|
|
230
242
|
onRedo: this.onRedo(),
|
|
@@ -232,11 +244,11 @@ let OGridService = class OGridService {
|
|
|
232
244
|
canRedo: this.canRedo(),
|
|
233
245
|
rowSelection: this.rowSelection(),
|
|
234
246
|
selectedRows: this.effectiveSelectedRows(),
|
|
235
|
-
onSelectionChange:
|
|
247
|
+
onSelectionChange: this.handleSelectionChangeFn,
|
|
236
248
|
statusBar: this.statusBarConfig(),
|
|
237
249
|
isLoading: this.isLoadingResolved(),
|
|
238
250
|
filters: this.filters(),
|
|
239
|
-
onFilterChange:
|
|
251
|
+
onFilterChange: this.handleFilterChangeFn,
|
|
240
252
|
filterOptions: this.clientFilterOptions(),
|
|
241
253
|
loadingFilterOptions: this.dataSource()?.fetchFilterOptions ? this.loadingFilterOptions() : EMPTY_LOADING_OPTIONS,
|
|
242
254
|
peopleSearch: this.dataSource()?.searchPeople?.bind(this.dataSource()),
|
|
@@ -249,7 +261,7 @@ let OGridService = class OGridService {
|
|
|
249
261
|
'aria-labelledby': this.ariaLabelledBy(),
|
|
250
262
|
emptyState: {
|
|
251
263
|
hasActiveFilters: this.hasActiveFilters(),
|
|
252
|
-
onClearAll:
|
|
264
|
+
onClearAll: this.clearAllFiltersFn,
|
|
253
265
|
message: this.emptyState()?.message,
|
|
254
266
|
render: this.emptyState()?.render,
|
|
255
267
|
},
|
|
@@ -369,6 +381,24 @@ let OGridService = class OGridService {
|
|
|
369
381
|
this.sideBarActivePanel.set(parsed.defaultPanel);
|
|
370
382
|
}
|
|
371
383
|
});
|
|
384
|
+
// Cleanup on destroy — reset callback signals to prevent closure retention
|
|
385
|
+
this.destroyRef.onDestroy(() => {
|
|
386
|
+
this.onPageChange.set(undefined);
|
|
387
|
+
this.onPageSizeChange.set(undefined);
|
|
388
|
+
this.onSortChange.set(undefined);
|
|
389
|
+
this.onFiltersChange.set(undefined);
|
|
390
|
+
this.onVisibleColumnsChange.set(undefined);
|
|
391
|
+
this.onColumnOrderChange.set(undefined);
|
|
392
|
+
this.onColumnResized.set(undefined);
|
|
393
|
+
this.onAutosizeColumn.set(undefined);
|
|
394
|
+
this.onColumnPinned.set(undefined);
|
|
395
|
+
this.onCellValueChanged.set(undefined);
|
|
396
|
+
this.onSelectionChange.set(undefined);
|
|
397
|
+
this.onFirstDataRendered.set(undefined);
|
|
398
|
+
this.onError.set(undefined);
|
|
399
|
+
this.onUndo.set(undefined);
|
|
400
|
+
this.onRedo.set(undefined);
|
|
401
|
+
});
|
|
372
402
|
}
|
|
373
403
|
// --- Setters ---
|
|
374
404
|
setPage(p) {
|
|
@@ -469,6 +499,8 @@ let OGridService = class OGridService {
|
|
|
469
499
|
this.onColumnOrderChange.set(props.onColumnOrderChange);
|
|
470
500
|
if (props.onColumnResized)
|
|
471
501
|
this.onColumnResized.set(props.onColumnResized);
|
|
502
|
+
if (props.onAutosizeColumn)
|
|
503
|
+
this.onAutosizeColumn.set(props.onAutosizeColumn);
|
|
472
504
|
if (props.onColumnPinned)
|
|
473
505
|
this.onColumnPinned.set(props.onColumnPinned);
|
|
474
506
|
if (props.defaultPageSize !== undefined)
|
|
@@ -483,6 +515,8 @@ let OGridService = class OGridService {
|
|
|
483
515
|
this.cellSelection.set(props.cellSelection);
|
|
484
516
|
if (props.density !== undefined)
|
|
485
517
|
this.density.set(props.density);
|
|
518
|
+
if (props.rowHeight !== undefined)
|
|
519
|
+
this.rowHeight.set(props.rowHeight);
|
|
486
520
|
if (props.onCellValueChanged)
|
|
487
521
|
this.onCellValueChanged.set(props.onCellValueChanged);
|
|
488
522
|
if (props.onUndo)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OGrid CSS theme variables (light + dark mode).
|
|
3
|
+
* Used by all Angular UI packages (Material, PrimeNG, Radix) to avoid duplication.
|
|
4
|
+
*/
|
|
5
|
+
export const OGRID_THEME_VARS_CSS = `
|
|
6
|
+
/* ─── OGrid Theme Variables ─── */
|
|
7
|
+
:root {
|
|
8
|
+
--ogrid-bg: #ffffff;
|
|
9
|
+
--ogrid-fg: rgba(0, 0, 0, 0.87);
|
|
10
|
+
--ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
|
|
11
|
+
--ogrid-fg-muted: rgba(0, 0, 0, 0.5);
|
|
12
|
+
--ogrid-border: rgba(0, 0, 0, 0.12);
|
|
13
|
+
--ogrid-header-bg: rgba(0, 0, 0, 0.04);
|
|
14
|
+
--ogrid-hover-bg: rgba(0, 0, 0, 0.04);
|
|
15
|
+
--ogrid-selected-row-bg: #e6f0fb;
|
|
16
|
+
--ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
|
|
17
|
+
--ogrid-range-bg: rgba(33, 115, 70, 0.12);
|
|
18
|
+
--ogrid-accent: #0078d4;
|
|
19
|
+
--ogrid-selection-color: #217346;
|
|
20
|
+
--ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
|
|
21
|
+
}
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not([data-theme="light"]) {
|
|
24
|
+
--ogrid-bg: #1e1e1e;
|
|
25
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
26
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
27
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
28
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
29
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
30
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
31
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
32
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
33
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
34
|
+
--ogrid-accent: #4da6ff;
|
|
35
|
+
--ogrid-selection-color: #2ea043;
|
|
36
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
[data-theme="dark"] {
|
|
40
|
+
--ogrid-bg: #1e1e1e;
|
|
41
|
+
--ogrid-fg: rgba(255, 255, 255, 0.87);
|
|
42
|
+
--ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
|
|
43
|
+
--ogrid-fg-muted: rgba(255, 255, 255, 0.5);
|
|
44
|
+
--ogrid-border: rgba(255, 255, 255, 0.12);
|
|
45
|
+
--ogrid-header-bg: rgba(255, 255, 255, 0.06);
|
|
46
|
+
--ogrid-hover-bg: rgba(255, 255, 255, 0.08);
|
|
47
|
+
--ogrid-selected-row-bg: #1a3a5c;
|
|
48
|
+
--ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
|
|
49
|
+
--ogrid-range-bg: rgba(46, 160, 67, 0.15);
|
|
50
|
+
--ogrid-accent: #4da6ff;
|
|
51
|
+
--ogrid-selection-color: #2ea043;
|
|
52
|
+
--ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
|
|
53
|
+
}`;
|
|
@@ -19,6 +19,8 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
19
19
|
readonly virtualScrollService: VirtualScrollService;
|
|
20
20
|
protected lastMouseShift: boolean;
|
|
21
21
|
readonly columnSizingVersion: import("@angular/core").WritableSignal<number>;
|
|
22
|
+
/** Dirty flag — set when column layout changes, cleared after measurement. */
|
|
23
|
+
private measureDirty;
|
|
22
24
|
/** DOM-measured column widths from the last layout pass.
|
|
23
25
|
* Used as a minWidth floor to prevent columns from shrinking
|
|
24
26
|
* when new data loads (e.g. server-side pagination). */
|
|
@@ -33,7 +35,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
33
35
|
protected abstract getTableContainerRef(): ElementRef<HTMLElement> | undefined;
|
|
34
36
|
/** Lifecycle hook — populate element signals from @ViewChild refs */
|
|
35
37
|
ngAfterViewInit(): void;
|
|
36
|
-
/** Lifecycle hook — re-measure column widths
|
|
38
|
+
/** Lifecycle hook — re-measure column widths only when layout changed */
|
|
37
39
|
ngAfterViewChecked(): void;
|
|
38
40
|
/** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
|
|
39
41
|
* Only updates the signal when values actually change, to avoid render loops. */
|
|
@@ -46,6 +48,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
46
48
|
readonly isLoading: import("@angular/core").Signal<boolean>;
|
|
47
49
|
readonly loadingMessage: import("@angular/core").Signal<string>;
|
|
48
50
|
readonly layoutModeFit: import("@angular/core").Signal<boolean>;
|
|
51
|
+
readonly rowHeightCssVar: import("@angular/core").Signal<string | null>;
|
|
49
52
|
readonly ariaLabel: import("@angular/core").Signal<string>;
|
|
50
53
|
readonly ariaLabelledBy: import("@angular/core").Signal<string | undefined>;
|
|
51
54
|
readonly emptyState: import("@angular/core").Signal<{
|
|
@@ -169,10 +172,46 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
169
172
|
visibleColIndex(col: IColumnDef<T>): number;
|
|
170
173
|
getColumnWidth(col: IColumnDef<T>): number;
|
|
171
174
|
getFilterConfig(col: IColumnDef<T>): HeaderFilterConfig;
|
|
175
|
+
/** Memoized column menu handlers — avoids recreating objects on every CD cycle */
|
|
176
|
+
protected readonly columnMenuHandlersMap: import("@angular/core").Signal<Map<string, {
|
|
177
|
+
onPinLeft: () => void;
|
|
178
|
+
onPinRight: () => void;
|
|
179
|
+
onUnpin: () => void;
|
|
180
|
+
onSortAsc: () => void;
|
|
181
|
+
onSortDesc: () => void;
|
|
182
|
+
onClearSort: () => void;
|
|
183
|
+
onAutosizeThis: () => void;
|
|
184
|
+
onAutosizeAll: () => void;
|
|
185
|
+
onClose: () => void;
|
|
186
|
+
}>>;
|
|
187
|
+
/** Build column menu handler object for a single column */
|
|
188
|
+
private buildColumnMenuHandlers;
|
|
189
|
+
/** Get memoized handlers for a column */
|
|
190
|
+
getColumnMenuHandlersMemoized(columnId: string): {
|
|
191
|
+
onPinLeft: () => void;
|
|
192
|
+
onPinRight: () => void;
|
|
193
|
+
onUnpin: () => void;
|
|
194
|
+
onSortAsc: () => void;
|
|
195
|
+
onSortDesc: () => void;
|
|
196
|
+
onClearSort: () => void;
|
|
197
|
+
onAutosizeThis: () => void;
|
|
198
|
+
onAutosizeAll: () => void;
|
|
199
|
+
onClose: () => void;
|
|
200
|
+
};
|
|
172
201
|
getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
|
|
173
202
|
resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): unknown;
|
|
174
203
|
resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
|
|
175
204
|
buildPopoverEditorProps(item: T, col: IColumnDef<T>, descriptor: CellRenderDescriptor): unknown;
|
|
205
|
+
/** Check if a specific cell is the active cell (PrimeNG inline template helper). */
|
|
206
|
+
isActiveCell(rowIndex: number, colIdx: number): boolean;
|
|
207
|
+
/** Check if a cell is within the current selection range (PrimeNG inline template helper). */
|
|
208
|
+
isInSelectionRange(rowIndex: number, colIdx: number): boolean;
|
|
209
|
+
/** Check if a cell is the selection end cell for fill handle display. */
|
|
210
|
+
isSelectionEndCell(rowIndex: number, colIdx: number): boolean;
|
|
211
|
+
/** Get cell background color based on selection state. */
|
|
212
|
+
getCellBackground(rowIndex: number, colIdx: number): string | null;
|
|
213
|
+
/** Resolve editor type from column definition. */
|
|
214
|
+
getEditorType(col: IColumnDef<T>, _item: T): 'text' | 'select' | 'checkbox' | 'date' | 'richSelect';
|
|
176
215
|
getSelectValues(col: IColumnDef<T>): string[];
|
|
177
216
|
formatDateForInput(value: unknown): string;
|
|
178
217
|
getPinnedLeftOffset(columnId: string): number | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IColumnDef, ICellEditorProps } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Shared popover cell editor template used by all Angular UI packages.
|
|
4
|
+
*/
|
|
5
|
+
export declare const POPOVER_CELL_EDITOR_TEMPLATE = "\n <div #anchorEl\n class=\"ogrid-popover-anchor\"\n [attr.data-row-index]=\"rowIndex\"\n [attr.data-col-index]=\"globalColIndex\"\n >\n {{ displayValue }}\n </div>\n @if (showEditor()) {\n <div class=\"ogrid-popover-editor-overlay\" (click)=\"handleOverlayClick()\">\n <div class=\"ogrid-popover-editor-content\" #editorContainer></div>\n </div>\n }\n";
|
|
6
|
+
/**
|
|
7
|
+
* Shared overlay + content styles for popover cell editors.
|
|
8
|
+
* Subclasses provide their own .ogrid-popover-anchor styles.
|
|
9
|
+
*/
|
|
10
|
+
export declare const POPOVER_CELL_EDITOR_OVERLAY_STYLES = "\n :host { display: contents; }\n .ogrid-popover-editor-overlay {\n position: fixed; inset: 0; z-index: 1000;\n background: rgba(0,0,0,0.3);\n display: flex; align-items: center; justify-content: center;\n }\n .ogrid-popover-editor-content {\n background: var(--ogrid-bg, #ffffff); border-radius: 4px; padding: 16px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n max-width: 90vw; max-height: 90vh; overflow: auto;\n color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));\n }\n";
|
|
11
|
+
/**
|
|
12
|
+
* Abstract base class for Angular popover cell editors.
|
|
13
|
+
* Contains all shared inputs, ViewChild refs, effects, and overlay click handling.
|
|
14
|
+
*
|
|
15
|
+
* Subclasses only need a @Component decorator with selector, template, and
|
|
16
|
+
* framework-specific .ogrid-popover-anchor CSS styles.
|
|
17
|
+
*/
|
|
18
|
+
export declare abstract class BasePopoverCellEditorComponent<T = unknown> {
|
|
19
|
+
item: T;
|
|
20
|
+
column: IColumnDef<T>;
|
|
21
|
+
rowIndex: number;
|
|
22
|
+
globalColIndex: number;
|
|
23
|
+
displayValue: unknown;
|
|
24
|
+
editorProps: ICellEditorProps<T>;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
private anchorRef?;
|
|
27
|
+
private editorContainerRef?;
|
|
28
|
+
private readonly injector;
|
|
29
|
+
private readonly envInjector;
|
|
30
|
+
protected readonly showEditor: import("@angular/core").WritableSignal<boolean>;
|
|
31
|
+
constructor();
|
|
32
|
+
protected handleOverlayClick(): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared inline cell editor template used by all Angular UI packages.
|
|
3
|
+
* The template is identical across Material, PrimeNG, and Radix implementations.
|
|
4
|
+
*/
|
|
5
|
+
export declare const INLINE_CELL_EDITOR_TEMPLATE = "\n @switch (editorType) {\n @case ('text') {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @case ('richSelect') {\n <div #richSelectWrapper\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\">\n <input\n #richSelectInput\n type=\"text\"\n [value]=\"searchText()\"\n (input)=\"onRichSelectSearch($any($event.target).value)\"\n (keydown)=\"onRichSelectKeyDown($event)\"\n placeholder=\"Search...\"\n style=\"width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0\"\n />\n <div #richSelectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of filteredOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n @if (filteredOptions().length === 0) {\n <div style=\"padding:6px 8px;color:var(--ogrid-muted, #999)\">No matches</div>\n }\n </div>\n </div>\n }\n @case ('select') {\n <div #selectWrapper tabindex=\"0\"\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\"\n (keydown)=\"onCustomSelectKeyDown($event)\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit\">\n <span>{{ getDisplayText(value) }}</span>\n <span style=\"margin-left:4px;font-size:10px;opacity:0.5\">▾</span>\n </div>\n <div #selectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of selectOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n </div>\n </div>\n }\n @case ('checkbox') {\n <div style=\"display:flex;align-items:center;justify-content:center;width:100%;height:100%\">\n <input\n type=\"checkbox\"\n [checked]=\"!!localValue()\"\n (change)=\"commitValue($any($event.target).checked)\"\n (keydown)=\"onCheckboxKeyDown($event)\"\n />\n </div>\n }\n @case ('date') {\n <input\n #inputEl\n type=\"date\"\n [value]=\"localValue()\"\n (change)=\"commitValue($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @default {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n }\n";
|
|
6
|
+
export declare const INLINE_CELL_EDITOR_STYLES = "\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,5 +24,8 @@ export { BaseColumnChooserComponent } from './components/base-column-chooser.com
|
|
|
24
24
|
export type { IColumnChooserProps } from './components/base-column-chooser.component';
|
|
25
25
|
export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
|
|
26
26
|
export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
|
|
27
|
+
export { INLINE_CELL_EDITOR_TEMPLATE, INLINE_CELL_EDITOR_STYLES } from './components/inline-cell-editor-template';
|
|
28
|
+
export { BasePopoverCellEditorComponent, POPOVER_CELL_EDITOR_TEMPLATE, POPOVER_CELL_EDITOR_OVERLAY_STYLES } from './components/base-popover-cell-editor.component';
|
|
29
|
+
export { OGRID_THEME_VARS_CSS } from './styles/ogrid-theme-vars';
|
|
27
30
|
export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
|
|
28
31
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -21,6 +21,8 @@ export interface DataGridLayoutState<T> {
|
|
|
21
21
|
widthPx: number;
|
|
22
22
|
}>) => void;
|
|
23
23
|
onColumnResized?: (columnId: string, width: number) => void;
|
|
24
|
+
/** Called when user requests autosize for a single column (with measured width). */
|
|
25
|
+
onAutosizeColumn?: (columnId: string, width: number) => void;
|
|
24
26
|
}
|
|
25
27
|
export interface DataGridRowSelectionState {
|
|
26
28
|
selectedRowIds: Set<RowId>;
|
|
@@ -113,7 +115,7 @@ export interface DataGridViewModelState<T> {
|
|
|
113
115
|
};
|
|
114
116
|
statusBarConfig: IStatusBarProps | null;
|
|
115
117
|
showEmptyInGrid: boolean;
|
|
116
|
-
onCellError?: (error: Error) => void;
|
|
118
|
+
onCellError?: (error: Error, info: unknown) => void;
|
|
117
119
|
}
|
|
118
120
|
/** Column pinning state and column header menu. */
|
|
119
121
|
export interface DataGridPinningState {
|
|
@@ -152,6 +154,7 @@ export interface DataGridStateResult<T> {
|
|
|
152
154
|
*/
|
|
153
155
|
export declare class DataGridStateService<T> {
|
|
154
156
|
private destroyRef;
|
|
157
|
+
private ngZone;
|
|
155
158
|
readonly props: import("@angular/core").WritableSignal<IOGridDataGridProps<T> | null>;
|
|
156
159
|
readonly wrapperEl: import("@angular/core").WritableSignal<HTMLElement | null>;
|
|
157
160
|
private readonly editingCellSig;
|
|
@@ -171,6 +174,9 @@ export declare class DataGridStateService<T> {
|
|
|
171
174
|
private readonly undoLengthSig;
|
|
172
175
|
private readonly redoLengthSig;
|
|
173
176
|
private fillDragStart;
|
|
177
|
+
private fillRafId;
|
|
178
|
+
private fillMoveHandler;
|
|
179
|
+
private fillUpHandler;
|
|
174
180
|
private lastClickedRow;
|
|
175
181
|
private dragStartPos;
|
|
176
182
|
private dragMoved;
|
|
@@ -185,6 +191,8 @@ export declare class DataGridStateService<T> {
|
|
|
185
191
|
private readonly headerMenuAnchorElementSig;
|
|
186
192
|
private readonly propsResolved;
|
|
187
193
|
readonly cellSelection: import("@angular/core").Signal<boolean>;
|
|
194
|
+
private readonly originalOnCellValueChanged;
|
|
195
|
+
private readonly initialColumnWidthsSig;
|
|
188
196
|
private readonly wrappedOnCellValueChanged;
|
|
189
197
|
readonly flatColumnsRaw: import("@angular/core").Signal<IColumnDef<T>[]>;
|
|
190
198
|
readonly flatColumns: import("@angular/core").Signal<IColumnDef<T>[]>;
|
|
@@ -68,6 +68,7 @@ export declare class OGridService<T> {
|
|
|
68
68
|
readonly columnOrder: import("@angular/core").WritableSignal<string[] | undefined>;
|
|
69
69
|
readonly onColumnOrderChange: import("@angular/core").WritableSignal<((order: string[]) => void) | undefined>;
|
|
70
70
|
readonly onColumnResized: import("@angular/core").WritableSignal<((columnId: string, width: number) => void) | undefined>;
|
|
71
|
+
readonly onAutosizeColumn: import("@angular/core").WritableSignal<((columnId: string, width: number) => void) | undefined>;
|
|
71
72
|
readonly onColumnPinned: import("@angular/core").WritableSignal<((columnId: string, pinned: "left" | "right" | null) => void) | undefined>;
|
|
72
73
|
readonly defaultPageSize: import("@angular/core").WritableSignal<number>;
|
|
73
74
|
readonly defaultSortBy: import("@angular/core").WritableSignal<string | undefined>;
|
|
@@ -85,6 +86,7 @@ export declare class OGridService<T> {
|
|
|
85
86
|
readonly editable: import("@angular/core").WritableSignal<boolean | undefined>;
|
|
86
87
|
readonly cellSelection: import("@angular/core").WritableSignal<boolean | undefined>;
|
|
87
88
|
readonly density: import("@angular/core").WritableSignal<"compact" | "normal" | "comfortable">;
|
|
89
|
+
readonly rowHeight: import("@angular/core").WritableSignal<number | undefined>;
|
|
88
90
|
readonly onCellValueChanged: import("@angular/core").WritableSignal<((event: ICellValueChangedEvent<T>) => void) | undefined>;
|
|
89
91
|
readonly onUndo: import("@angular/core").WritableSignal<(() => void) | undefined>;
|
|
90
92
|
readonly onRedo: import("@angular/core").WritableSignal<(() => void) | undefined>;
|
|
@@ -164,6 +166,12 @@ export declare class OGridService<T> {
|
|
|
164
166
|
filterType: "text" | "multiSelect" | "people" | "date";
|
|
165
167
|
}[]>;
|
|
166
168
|
readonly sideBarState: import("@angular/core").Signal<OGridSideBarState>;
|
|
169
|
+
private readonly handleSortFn;
|
|
170
|
+
private readonly handleColumnResizedFn;
|
|
171
|
+
private readonly handleColumnPinnedFn;
|
|
172
|
+
private readonly handleSelectionChangeFn;
|
|
173
|
+
private readonly handleFilterChangeFn;
|
|
174
|
+
private readonly clearAllFiltersFn;
|
|
167
175
|
readonly dataGridProps: import("@angular/core").Signal<IOGridDataGridProps<T>>;
|
|
168
176
|
readonly pagination: import("@angular/core").Signal<OGridPagination>;
|
|
169
177
|
readonly columnChooser: import("@angular/core").Signal<OGridColumnChooser>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OGrid CSS theme variables (light + dark mode).
|
|
3
|
+
* Used by all Angular UI packages (Material, PrimeNG, Radix) to avoid duplication.
|
|
4
|
+
*/
|
|
5
|
+
export declare const OGRID_THEME_VARS_CSS = "\n/* \u2500\u2500\u2500 OGrid Theme Variables \u2500\u2500\u2500 */\n:root {\n --ogrid-bg: #ffffff;\n --ogrid-fg: rgba(0, 0, 0, 0.87);\n --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);\n --ogrid-fg-muted: rgba(0, 0, 0, 0.5);\n --ogrid-border: rgba(0, 0, 0, 0.12);\n --ogrid-header-bg: rgba(0, 0, 0, 0.04);\n --ogrid-hover-bg: rgba(0, 0, 0, 0.04);\n --ogrid-selected-row-bg: #e6f0fb;\n --ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);\n --ogrid-range-bg: rgba(33, 115, 70, 0.12);\n --ogrid-accent: #0078d4;\n --ogrid-selection-color: #217346;\n --ogrid-loading-overlay: rgba(255, 255, 255, 0.7);\n}\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n --ogrid-bg: #1e1e1e;\n --ogrid-fg: rgba(255, 255, 255, 0.87);\n --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);\n --ogrid-fg-muted: rgba(255, 255, 255, 0.5);\n --ogrid-border: rgba(255, 255, 255, 0.12);\n --ogrid-header-bg: rgba(255, 255, 255, 0.06);\n --ogrid-hover-bg: rgba(255, 255, 255, 0.08);\n --ogrid-selected-row-bg: #1a3a5c;\n --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);\n --ogrid-range-bg: rgba(46, 160, 67, 0.15);\n --ogrid-accent: #4da6ff;\n --ogrid-selection-color: #2ea043;\n --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);\n }\n}\n[data-theme=\"dark\"] {\n --ogrid-bg: #1e1e1e;\n --ogrid-fg: rgba(255, 255, 255, 0.87);\n --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);\n --ogrid-fg-muted: rgba(255, 255, 255, 0.5);\n --ogrid-border: rgba(255, 255, 255, 0.12);\n --ogrid-header-bg: rgba(255, 255, 255, 0.06);\n --ogrid-hover-bg: rgba(255, 255, 255, 0.08);\n --ogrid-selected-row-bg: #1a3a5c;\n --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);\n --ogrid-range-bg: rgba(46, 160, 67, 0.15);\n --ogrid-accent: #4da6ff;\n --ogrid-selection-color: #2ea043;\n --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);\n}";
|
|
@@ -27,6 +27,8 @@ interface IOGridBaseProps<T> {
|
|
|
27
27
|
columnOrder?: string[];
|
|
28
28
|
onColumnOrderChange?: (order: string[]) => void;
|
|
29
29
|
onColumnResized?: (columnId: string, width: number) => void;
|
|
30
|
+
/** Called when user requests autosize for a single column (with measured width). */
|
|
31
|
+
onAutosizeColumn?: (columnId: string, width: number) => void;
|
|
30
32
|
onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
|
|
31
33
|
editable?: boolean;
|
|
32
34
|
cellSelection?: boolean;
|
|
@@ -57,10 +59,13 @@ interface IOGridBaseProps<T> {
|
|
|
57
59
|
sideBar?: boolean | ISideBarDef;
|
|
58
60
|
columnReorder?: boolean;
|
|
59
61
|
virtualScroll?: IVirtualScrollConfig;
|
|
62
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
63
|
+
rowHeight?: number;
|
|
60
64
|
pageSizeOptions?: number[];
|
|
61
65
|
onFirstDataRendered?: () => void;
|
|
62
66
|
onError?: (error: unknown) => void;
|
|
63
|
-
onCellError?: (error: Error) => void;
|
|
67
|
+
onCellError?: (error: Error, info: unknown) => void;
|
|
68
|
+
showRowNumbers?: boolean;
|
|
64
69
|
'aria-label'?: string;
|
|
65
70
|
'aria-labelledby'?: string;
|
|
66
71
|
}
|
|
@@ -89,6 +94,8 @@ export interface IOGridDataGridProps<T> {
|
|
|
89
94
|
columnOrder?: string[];
|
|
90
95
|
onColumnOrderChange?: (order: string[]) => void;
|
|
91
96
|
onColumnResized?: (columnId: string, width: number) => void;
|
|
97
|
+
/** Called when user requests autosize for a single column (with measured width). */
|
|
98
|
+
onAutosizeColumn?: (columnId: string, width: number) => void;
|
|
92
99
|
onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
|
|
93
100
|
pinnedColumns?: Record<string, 'left' | 'right'>;
|
|
94
101
|
initialColumnWidths?: Record<string, number>;
|
|
@@ -119,13 +126,15 @@ export interface IOGridDataGridProps<T> {
|
|
|
119
126
|
getUserByEmail?: (email: string) => Promise<UserLike | undefined>;
|
|
120
127
|
columnReorder?: boolean;
|
|
121
128
|
virtualScroll?: IVirtualScrollConfig;
|
|
129
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
130
|
+
rowHeight?: number;
|
|
122
131
|
emptyState?: {
|
|
123
132
|
onClearAll: () => void;
|
|
124
133
|
hasActiveFilters: boolean;
|
|
125
134
|
message?: string;
|
|
126
135
|
render?: TemplateRef<unknown>;
|
|
127
136
|
};
|
|
128
|
-
onCellError?: (error: Error) => void;
|
|
137
|
+
onCellError?: (error: Error, info: unknown) => void;
|
|
129
138
|
'aria-label'?: string;
|
|
130
139
|
'aria-labelledby'?: string;
|
|
131
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-angular",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
4
4
|
"description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
38
|
+
"@alaarab/ogrid-core": "2.0.22"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@angular/core": "^21.0.0",
|