@alaarab/ogrid-angular-material 2.0.2

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.
@@ -0,0 +1,753 @@
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 { Component, input, computed, effect, ChangeDetectionStrategy, viewChild, } from '@angular/core';
8
+ import { DataGridStateService, buildHeaderRows, getCellValue, isInSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-angular';
9
+ import { ColumnHeaderFilterComponent } from '../column-header-filter/column-header-filter.component';
10
+ function getHeaderFilterConfig(col, input) {
11
+ const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
12
+ const filterType = (filterable?.type ?? 'none');
13
+ const filterField = filterable?.filterField ?? col.columnId;
14
+ const sortable = col.sortable !== false;
15
+ const filterValue = input.filters[filterField];
16
+ const base = {
17
+ columnKey: col.columnId,
18
+ columnName: col.name,
19
+ filterType,
20
+ isSorted: input.sortBy === col.columnId,
21
+ isSortedDescending: input.sortBy === col.columnId && input.sortDirection === 'desc',
22
+ onSort: sortable ? () => input.onColumnSort(col.columnId) : undefined,
23
+ };
24
+ if (filterType === 'text') {
25
+ return {
26
+ ...base,
27
+ textValue: filterValue?.type === 'text' ? filterValue.value : '',
28
+ onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
29
+ };
30
+ }
31
+ if (filterType === 'people') {
32
+ return {
33
+ ...base,
34
+ selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
35
+ onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
36
+ peopleSearch: input.peopleSearch,
37
+ };
38
+ }
39
+ if (filterType === 'multiSelect') {
40
+ return {
41
+ ...base,
42
+ options: input.filterOptions[filterField] ?? [],
43
+ isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
44
+ selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
45
+ onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
46
+ };
47
+ }
48
+ if (filterType === 'date') {
49
+ return {
50
+ ...base,
51
+ dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
52
+ onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
53
+ };
54
+ }
55
+ return base;
56
+ }
57
+ function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
58
+ const rowId = input.getRowId(item);
59
+ const globalColIndex = colIdx + input.colOffset;
60
+ const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
61
+ const canEditInline = input.editable !== false && !!colEditable && !!input.onCellValueChanged && typeof col.cellEditor !== 'function';
62
+ const canEditPopup = input.editable !== false && !!colEditable && !!input.onCellValueChanged && typeof col.cellEditor === 'function';
63
+ const canEditAny = canEditInline || canEditPopup;
64
+ const isEditing = input.editingCell?.rowId === rowId && input.editingCell?.columnId === col.columnId;
65
+ const isActive = input.activeCell?.rowIndex === rowIndex && input.activeCell?.columnIndex === globalColIndex;
66
+ const inRange = input.selectionRange != null && isInSelectionRange(input.selectionRange, rowIndex, colIdx);
67
+ const isInCutRange = input.cutRange != null && isInSelectionRange(input.cutRange, rowIndex, colIdx);
68
+ const isSelectionEndCell = !input.isDragging && input.copyRange == null && input.cutRange == null &&
69
+ input.selectionRange != null && rowIndex === input.selectionRange.endRow && colIdx === input.selectionRange.endCol;
70
+ let mode = 'display';
71
+ let editorType;
72
+ const value = getCellValue(item, col);
73
+ if (isEditing && canEditInline) {
74
+ mode = 'editing-inline';
75
+ if (col.cellEditor === 'text' || col.cellEditor === 'select' || col.cellEditor === 'checkbox' || col.cellEditor === 'richSelect' || col.cellEditor === 'date') {
76
+ editorType = col.cellEditor;
77
+ }
78
+ else if (col.type === 'date')
79
+ editorType = 'date';
80
+ else if (col.type === 'boolean')
81
+ editorType = 'checkbox';
82
+ else
83
+ editorType = 'text';
84
+ }
85
+ else if (isEditing && canEditPopup) {
86
+ mode = 'editing-popover';
87
+ }
88
+ return {
89
+ mode, editorType, value,
90
+ isActive, isInRange: inRange, isInCutRange, isSelectionEndCell,
91
+ canEditAny, globalColIndex, rowId, rowIndex, displayValue: value,
92
+ };
93
+ }
94
+ function resolveCellDisplayContent(col, item, displayValue) {
95
+ if (col.renderCell && typeof col.renderCell === 'function') {
96
+ const result = col.renderCell(item);
97
+ return result != null ? String(result) : '';
98
+ }
99
+ if (col.valueFormatter)
100
+ return String(col.valueFormatter(displayValue, item) ?? '');
101
+ if (displayValue == null)
102
+ return '';
103
+ if (col.type === 'date') {
104
+ const d = new Date(String(displayValue));
105
+ if (!Number.isNaN(d.getTime()))
106
+ return d.toLocaleDateString();
107
+ }
108
+ if (col.type === 'boolean')
109
+ return displayValue ? 'True' : 'False';
110
+ return String(displayValue);
111
+ }
112
+ function resolveCellStyle(col, item) {
113
+ if (!col.cellStyle)
114
+ return undefined;
115
+ return typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle;
116
+ }
117
+ /**
118
+ * DataGridTable component using native HTML table with Material Design-inspired styling.
119
+ * Standalone component — this is the workhorse of the grid.
120
+ */
121
+ let DataGridTableComponent = class DataGridTableComponent {
122
+ constructor() {
123
+ this.propsInput = input.required({ alias: 'props' });
124
+ this.wrapperRef = viewChild('wrapperEl');
125
+ this.tableContainerRef = viewChild('tableContainerEl');
126
+ this.stateService = new DataGridStateService();
127
+ this.lastMouseShift = false;
128
+ // --- Delegated state ---
129
+ this.state = computed(() => this.stateService.getState());
130
+ this.items = computed(() => this.propsInput()?.items ?? []);
131
+ this.getRowId = computed(() => this.propsInput()?.getRowId ?? ((item) => item['id']));
132
+ this.isLoading = computed(() => this.propsInput()?.isLoading ?? false);
133
+ this.loadingMessage = computed(() => 'Loading\u2026');
134
+ this.freezeRows = computed(() => this.propsInput()?.freezeRows);
135
+ this.freezeCols = computed(() => this.propsInput()?.freezeCols);
136
+ this.layoutModeFit = computed(() => (this.propsInput()?.layoutMode ?? 'fill') === 'content');
137
+ this.ariaLabel = computed(() => this.propsInput()?.['aria-label'] ?? 'Data grid');
138
+ this.ariaLabelledBy = computed(() => this.propsInput()?.['aria-labelledby']);
139
+ this.emptyState = computed(() => this.propsInput()?.emptyState);
140
+ // State service outputs
141
+ this.visibleCols = computed(() => this.state().layout.visibleCols);
142
+ this.hasCheckboxCol = computed(() => this.state().layout.hasCheckboxCol);
143
+ this.colOffset = computed(() => this.state().layout.colOffset);
144
+ this.containerWidth = computed(() => this.state().layout.containerWidth);
145
+ this.minTableWidth = computed(() => this.state().layout.minTableWidth);
146
+ this.desiredTableWidth = computed(() => this.state().layout.desiredTableWidth);
147
+ this.columnSizingOverrides = computed(() => this.state().layout.columnSizingOverrides);
148
+ this.selectedRowIds = computed(() => this.state().rowSelection.selectedRowIds);
149
+ this.allSelected = computed(() => this.state().rowSelection.allSelected);
150
+ this.someSelected = computed(() => this.state().rowSelection.someSelected);
151
+ this.editingCell = computed(() => this.state().editing.editingCell);
152
+ this.pendingEditorValue = computed(() => this.state().editing.pendingEditorValue);
153
+ this.activeCell = computed(() => this.state().interaction.activeCell);
154
+ this.selectionRange = computed(() => this.state().interaction.selectionRange);
155
+ this.hasCellSelection = computed(() => this.state().interaction.hasCellSelection);
156
+ this.cutRange = computed(() => this.state().interaction.cutRange);
157
+ this.copyRange = computed(() => this.state().interaction.copyRange);
158
+ this.canUndo = computed(() => this.state().interaction.canUndo);
159
+ this.canRedo = computed(() => this.state().interaction.canRedo);
160
+ this.isDragging = computed(() => this.state().interaction.isDragging);
161
+ this.menuPosition = computed(() => this.state().contextMenu.menuPosition);
162
+ this.statusBarConfig = computed(() => this.state().viewModels.statusBarConfig);
163
+ this.showEmptyInGrid = computed(() => this.state().viewModels.showEmptyInGrid);
164
+ this.headerFilterInput = computed(() => this.state().viewModels.headerFilterInput);
165
+ this.cellDescriptorInput = computed(() => this.state().viewModels.cellDescriptorInput);
166
+ this.allowOverflowX = computed(() => {
167
+ const p = this.propsInput();
168
+ if (p?.suppressHorizontalScroll)
169
+ return false;
170
+ const cw = this.containerWidth();
171
+ const mtw = this.minTableWidth();
172
+ const dtw = this.desiredTableWidth();
173
+ return cw > 0 && (mtw > cw || dtw > cw);
174
+ });
175
+ this.selectionCellCount = computed(() => {
176
+ const sr = this.selectionRange();
177
+ if (!sr)
178
+ return undefined;
179
+ return (Math.abs(sr.endRow - sr.startRow) + 1) * (Math.abs(sr.endCol - sr.startCol) + 1);
180
+ });
181
+ // Header rows from column definition
182
+ this.headerRows = computed(() => {
183
+ const p = this.propsInput();
184
+ if (!p)
185
+ return [];
186
+ return buildHeaderRows(p.columns, p.visibleColumns);
187
+ });
188
+ // Pre-computed column layouts
189
+ this.columnLayouts = computed(() => {
190
+ const cols = this.visibleCols();
191
+ const fc = this.freezeCols();
192
+ return cols.map((col, colIdx) => {
193
+ const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
194
+ const pinnedLeft = col.pinned === 'left' || (isFreezeCol && colIdx === 0);
195
+ const pinnedRight = col.pinned === 'right';
196
+ const w = this.getColumnWidth(col);
197
+ return {
198
+ col,
199
+ pinnedLeft,
200
+ pinnedRight,
201
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
202
+ width: w,
203
+ };
204
+ });
205
+ });
206
+ // Wire props and wrapper element to state service
207
+ effect(() => {
208
+ const p = this.propsInput();
209
+ if (p)
210
+ this.stateService.props.set(p);
211
+ });
212
+ effect(() => {
213
+ const el = this.wrapperRef()?.nativeElement;
214
+ if (el)
215
+ this.stateService.wrapperEl.set(el);
216
+ });
217
+ }
218
+ // --- Helper methods ---
219
+ asColumnDef(colDef) {
220
+ return colDef;
221
+ }
222
+ visibleColIndex(col) {
223
+ return this.visibleCols().indexOf(col);
224
+ }
225
+ getColumnWidth(col) {
226
+ const overrides = this.columnSizingOverrides();
227
+ const override = overrides[col.columnId];
228
+ if (override)
229
+ return override.widthPx;
230
+ return col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
231
+ }
232
+ getFilterConfig(col) {
233
+ return getHeaderFilterConfig(col, this.headerFilterInput());
234
+ }
235
+ getCellDescriptor(item, col, rowIndex, colIdx) {
236
+ return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
237
+ }
238
+ resolveCellContent(col, item, displayValue) {
239
+ return resolveCellDisplayContent(col, item, displayValue);
240
+ }
241
+ resolveCellStyleFn(col, item) {
242
+ return resolveCellStyle(col, item);
243
+ }
244
+ getSelectValues(col) {
245
+ const params = col.cellEditorParams;
246
+ if (params && typeof params === 'object' && 'values' in params) {
247
+ return params.values.map(String);
248
+ }
249
+ return [];
250
+ }
251
+ formatDateForInput(value) {
252
+ if (!value)
253
+ return '';
254
+ const d = new Date(String(value));
255
+ if (Number.isNaN(d.getTime()))
256
+ return '';
257
+ return d.toISOString().split('T')[0];
258
+ }
259
+ // --- Event handlers ---
260
+ onWrapperMouseDown(event) {
261
+ this.lastMouseShift = event.shiftKey;
262
+ }
263
+ onGridKeyDown(event) {
264
+ this.state().interaction.handleGridKeyDown(event);
265
+ }
266
+ onCellMouseDown(event, rowIndex, globalColIndex) {
267
+ this.state().interaction.handleCellMouseDown(event, rowIndex, globalColIndex);
268
+ }
269
+ onCellClick(rowIndex, globalColIndex) {
270
+ this.state().interaction.setActiveCell({ rowIndex, columnIndex: globalColIndex });
271
+ }
272
+ onCellContextMenu(event) {
273
+ this.state().contextMenu.handleCellContextMenu(event);
274
+ }
275
+ onCellDblClick(rowId, columnId) {
276
+ this.state().editing.setEditingCell({ rowId, columnId });
277
+ }
278
+ onFillHandleMouseDown(event) {
279
+ this.state().interaction.handleFillHandleMouseDown(event);
280
+ }
281
+ onResizeStart(event, col) {
282
+ event.preventDefault();
283
+ const startX = event.clientX;
284
+ const startWidth = this.getColumnWidth(col);
285
+ const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
286
+ const onMove = (e) => {
287
+ const delta = e.clientX - startX;
288
+ const newWidth = Math.max(minWidth, startWidth + delta);
289
+ const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
290
+ this.state().layout.setColumnSizingOverrides(overrides);
291
+ };
292
+ const onUp = () => {
293
+ window.removeEventListener('mousemove', onMove);
294
+ window.removeEventListener('mouseup', onUp);
295
+ const finalWidth = this.getColumnWidth(col);
296
+ this.state().layout.onColumnResized?.(col.columnId, finalWidth);
297
+ };
298
+ window.addEventListener('mousemove', onMove);
299
+ window.addEventListener('mouseup', onUp);
300
+ }
301
+ onSelectAllChange(event) {
302
+ const checked = event.target.checked;
303
+ this.state().rowSelection.handleSelectAll(!!checked);
304
+ }
305
+ onRowClick(event, rowId) {
306
+ const p = this.propsInput();
307
+ if (p?.rowSelection !== 'single')
308
+ return;
309
+ const ids = this.selectedRowIds();
310
+ this.state().rowSelection.updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
311
+ }
312
+ onRowCheckboxChange(rowId, event, rowIndex) {
313
+ const checked = event.target.checked;
314
+ this.state().rowSelection.handleRowCheckboxChange(rowId, checked, rowIndex, this.lastMouseShift);
315
+ }
316
+ commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
317
+ this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
318
+ }
319
+ cancelEdit() {
320
+ this.state().editing.setEditingCell(null);
321
+ }
322
+ onEditorKeydown(event, item, columnId, oldValue, rowIndex, globalColIndex) {
323
+ if (event.key === 'Enter') {
324
+ event.preventDefault();
325
+ const newValue = event.target.value;
326
+ this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
327
+ }
328
+ else if (event.key === 'Escape') {
329
+ event.preventDefault();
330
+ this.cancelEdit();
331
+ }
332
+ }
333
+ closeContextMenu() {
334
+ this.state().contextMenu.closeContextMenu();
335
+ }
336
+ handleCopy() {
337
+ this.state().interaction.handleCopy();
338
+ }
339
+ handleCut() {
340
+ this.state().interaction.handleCut();
341
+ }
342
+ handlePaste() {
343
+ void this.state().interaction.handlePaste();
344
+ }
345
+ handleSelectAllCells() {
346
+ this.state().interaction.handleSelectAllCells();
347
+ }
348
+ onUndo() {
349
+ this.state().interaction.onUndo?.();
350
+ }
351
+ onRedo() {
352
+ this.state().interaction.onRedo?.();
353
+ }
354
+ };
355
+ DataGridTableComponent = __decorate([
356
+ Component({
357
+ selector: 'ogrid-datagrid-table',
358
+ standalone: true,
359
+ imports: [ColumnHeaderFilterComponent],
360
+ providers: [DataGridStateService],
361
+ changeDetection: ChangeDetectionStrategy.OnPush,
362
+ template: `
363
+ <div class="ogrid-datagrid-root">
364
+ <div
365
+ #wrapperEl
366
+ class="ogrid-datagrid-wrapper"
367
+ [class.ogrid-datagrid-wrapper--fit]="layoutModeFit()"
368
+ [class.ogrid-datagrid-wrapper--overflow-x]="allowOverflowX()"
369
+ tabindex="0"
370
+ role="region"
371
+ [attr.aria-label]="ariaLabel()"
372
+ [attr.aria-labelledby]="ariaLabelledBy()"
373
+ (mousedown)="onWrapperMouseDown($event)"
374
+ (keydown)="onGridKeyDown($event)"
375
+ (contextmenu)="$event.preventDefault()"
376
+ [attr.data-overflow-x]="allowOverflowX() ? 'true' : 'false'"
377
+ >
378
+ <div class="ogrid-datagrid-scroll-wrapper">
379
+ <div [style.minWidth.px]="allowOverflowX() ? minTableWidth() : undefined">
380
+ <div [class.ogrid-datagrid-table-wrapper--loading]="isLoading() && items().length > 0" #tableContainerEl>
381
+ <table class="ogrid-datagrid-table" [style.minWidth.px]="minTableWidth()"
382
+ [attr.data-freeze-rows]="freezeRows()"
383
+ [attr.data-freeze-cols]="freezeCols()"
384
+ >
385
+ <thead class="ogrid-datagrid-thead">
386
+ @for (row of headerRows(); track $index; let rowIdx = $index) {
387
+ <tr class="ogrid-datagrid-header-row">
388
+ @if (rowIdx === headerRows().length - 1 && hasCheckboxCol()) {
389
+ <th class="ogrid-datagrid-th ogrid-datagrid-checkbox-col" [attr.rowSpan]="headerRows().length > 1 ? 1 : null">
390
+ <div class="ogrid-datagrid-checkbox-wrapper">
391
+ <input
392
+ type="checkbox"
393
+ [checked]="allSelected()"
394
+ [indeterminate]="someSelected()"
395
+ (change)="onSelectAllChange($event)"
396
+ aria-label="Select all rows"
397
+ />
398
+ </div>
399
+ </th>
400
+ }
401
+ @if (rowIdx === 0 && rowIdx < headerRows().length - 1 && hasCheckboxCol()) {
402
+ <th [attr.rowSpan]="headerRows().length - 1" class="ogrid-datagrid-th" style="width: 48px; min-width: 48px; padding: 0;"></th>
403
+ }
404
+ @for (cell of row; track $index; let cellIdx = $index) {
405
+ @if (cell.isGroup) {
406
+ <th [attr.colSpan]="cell.colSpan" scope="colgroup" class="ogrid-datagrid-th ogrid-datagrid-group-header">
407
+ {{ cell.label }}
408
+ </th>
409
+ } @else {
410
+ @let col = asColumnDef(cell.columnDef);
411
+ @let colIdx = visibleColIndex(col);
412
+ @let isFreezeCol = freezeCols() != null && (freezeCols() ?? 0) >= 1 && colIdx < (freezeCols() ?? 0);
413
+ @let colW = getColumnWidth(col);
414
+ <th scope="col"
415
+ class="ogrid-datagrid-th"
416
+ [class.ogrid-datagrid-th--pinned-left]="col.pinned === 'left' || (isFreezeCol && colIdx === 0)"
417
+ [class.ogrid-datagrid-th--pinned-right]="col.pinned === 'right'"
418
+ [attr.rowSpan]="headerRows().length > 1 ? headerRows().length - rowIdx : null"
419
+ [style.minWidth.px]="col.minWidth ?? 80"
420
+ [style.width.px]="colW"
421
+ [style.maxWidth.px]="colW"
422
+ >
423
+ <ogrid-column-header-filter
424
+ [columnKey]="col.columnId"
425
+ [columnName]="col.name"
426
+ [filterType]="getFilterConfig(col).filterType"
427
+ [isSorted]="getFilterConfig(col).isSorted"
428
+ [isSortedDescending]="getFilterConfig(col).isSortedDescending"
429
+ [onSort]="getFilterConfig(col).onSort"
430
+ [selectedValues]="getFilterConfig(col).selectedValues"
431
+ [onFilterChange]="getFilterConfig(col).onFilterChange"
432
+ [options]="getFilterConfig(col).options"
433
+ [isLoadingOptions]="getFilterConfig(col).isLoadingOptions ?? false"
434
+ [textValue]="getFilterConfig(col).textValue ?? ''"
435
+ [onTextChange]="getFilterConfig(col).onTextChange"
436
+ [selectedUser]="getFilterConfig(col).selectedUser"
437
+ [onUserChange]="getFilterConfig(col).onUserChange"
438
+ [peopleSearch]="getFilterConfig(col).peopleSearch"
439
+ [dateValue]="getFilterConfig(col).dateValue"
440
+ [onDateChange]="getFilterConfig(col).onDateChange"
441
+ />
442
+ <div class="ogrid-datagrid-resize-handle" (mousedown)="onResizeStart($event, col)"></div>
443
+ </th>
444
+ }
445
+ }
446
+ </tr>
447
+ }
448
+ </thead>
449
+ @if (!showEmptyInGrid()) {
450
+ <tbody>
451
+ @for (item of items(); track getRowId()(item); let rowIndex = $index) {
452
+ @let rowId = getRowId()(item);
453
+ @let isSelected = selectedRowIds().has(rowId);
454
+ <tr
455
+ class="ogrid-datagrid-row"
456
+ [class.ogrid-datagrid-row--selected]="isSelected"
457
+ [attr.data-row-id]="rowId"
458
+ (click)="onRowClick($event, rowId)"
459
+ >
460
+ @if (hasCheckboxCol()) {
461
+ <td class="ogrid-datagrid-td ogrid-datagrid-checkbox-col">
462
+ <div
463
+ class="ogrid-datagrid-checkbox-wrapper"
464
+ [attr.data-row-index]="rowIndex"
465
+ [attr.data-col-index]="0"
466
+ (click)="$event.stopPropagation()"
467
+ >
468
+ <input
469
+ type="checkbox"
470
+ [checked]="isSelected"
471
+ (change)="onRowCheckboxChange(rowId, $event, rowIndex)"
472
+ [attr.aria-label]="'Select row ' + (rowIndex + 1)"
473
+ />
474
+ </div>
475
+ </td>
476
+ }
477
+ @for (colLayout of columnLayouts(); track colLayout.col.columnId; let colIdx = $index) {
478
+ <td
479
+ class="ogrid-datagrid-td"
480
+ [class.ogrid-datagrid-td--pinned-left]="colLayout.pinnedLeft"
481
+ [class.ogrid-datagrid-td--pinned-right]="colLayout.pinnedRight"
482
+ [style.minWidth.px]="colLayout.minWidth"
483
+ [style.width.px]="colLayout.width"
484
+ [style.maxWidth.px]="colLayout.width"
485
+ >
486
+ @let descriptor = getCellDescriptor(item, colLayout.col, rowIndex, colIdx);
487
+ @if (descriptor.mode === 'editing-inline') {
488
+ <div class="ogrid-datagrid-cell ogrid-datagrid-cell--editing">
489
+ @switch (descriptor.editorType) {
490
+ @case ('checkbox') {
491
+ <input
492
+ type="checkbox"
493
+ [checked]="!!descriptor.value"
494
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLInputElement).checked, rowIndex, descriptor.globalColIndex)"
495
+ (keydown)="$event.key === 'Escape' && cancelEdit()"
496
+ />
497
+ }
498
+ @case ('select') {
499
+ <select
500
+ class="ogrid-datagrid-editor-select"
501
+ [value]="descriptor.value != null ? '' + descriptor.value : ''"
502
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLSelectElement).value, rowIndex, descriptor.globalColIndex)"
503
+ (keydown)="$event.key === 'Escape' && cancelEdit()"
504
+ >
505
+ @for (v of getSelectValues(colLayout.col); track v) {
506
+ <option [value]="v">{{ v }}</option>
507
+ }
508
+ </select>
509
+ }
510
+ @case ('date') {
511
+ <input
512
+ type="date"
513
+ class="ogrid-datagrid-editor-input"
514
+ [value]="formatDateForInput(descriptor.value)"
515
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLInputElement).value, rowIndex, descriptor.globalColIndex)"
516
+ (keydown)="onEditorKeydown($event, item, colLayout.col.columnId, descriptor.value, rowIndex, descriptor.globalColIndex)"
517
+ />
518
+ }
519
+ @default {
520
+ <input
521
+ type="text"
522
+ class="ogrid-datagrid-editor-input"
523
+ [value]="descriptor.value != null ? '' + descriptor.value : ''"
524
+ (keydown)="onEditorKeydown($event, item, colLayout.col.columnId, descriptor.value, rowIndex, descriptor.globalColIndex)"
525
+ />
526
+ }
527
+ }
528
+ </div>
529
+ } @else {
530
+ @let content = resolveCellContent(colLayout.col, item, descriptor.displayValue);
531
+ @let cellStyle = resolveCellStyleFn(colLayout.col, item);
532
+ <div
533
+ class="ogrid-datagrid-cell"
534
+ [class.ogrid-datagrid-cell--active]="descriptor.isActive && !descriptor.isInRange"
535
+ [class.ogrid-datagrid-cell--in-range]="descriptor.isInRange"
536
+ [class.ogrid-datagrid-cell--in-cut-range]="descriptor.isInCutRange"
537
+ [class.ogrid-datagrid-cell--editable]="descriptor.canEditAny"
538
+ [class.ogrid-datagrid-cell--numeric]="colLayout.col.type === 'numeric'"
539
+ [class.ogrid-datagrid-cell--boolean]="colLayout.col.type === 'boolean'"
540
+ [attr.data-row-index]="rowIndex"
541
+ [attr.data-col-index]="descriptor.globalColIndex"
542
+ [attr.data-in-range]="descriptor.isInRange ? 'true' : null"
543
+ [attr.tabindex]="descriptor.isActive ? 0 : -1"
544
+ (mousedown)="onCellMouseDown($event, rowIndex, descriptor.globalColIndex)"
545
+ (click)="onCellClick(rowIndex, descriptor.globalColIndex)"
546
+ (contextmenu)="onCellContextMenu($event)"
547
+ (dblclick)="descriptor.canEditAny ? onCellDblClick(descriptor.rowId, colLayout.col.columnId) : null"
548
+ [attr.role]="descriptor.canEditAny ? 'button' : null"
549
+ [style]="cellStyle ?? undefined"
550
+ >
551
+ {{ content }}
552
+ @if (descriptor.canEditAny && descriptor.isSelectionEndCell) {
553
+ <div
554
+ class="ogrid-datagrid-fill-handle"
555
+ (mousedown)="onFillHandleMouseDown($event)"
556
+ aria-label="Fill handle"
557
+ ></div>
558
+ }
559
+ </div>
560
+ }
561
+ </td>
562
+ }
563
+ </tr>
564
+ }
565
+ </tbody>
566
+ }
567
+ </table>
568
+
569
+ @if (showEmptyInGrid() && emptyState()) {
570
+ <div class="ogrid-datagrid-empty">
571
+ <div class="ogrid-datagrid-empty__title">No results found</div>
572
+ <div class="ogrid-datagrid-empty__message">
573
+ @if (emptyState()!.message != null) {
574
+ {{ emptyState()!.message }}
575
+ } @else if (emptyState()!.hasActiveFilters) {
576
+ No items match your current filters. Try adjusting your search or
577
+ <button class="ogrid-datagrid-empty__clear" (click)="emptyState()!.onClearAll?.()">clear all filters</button>
578
+ to see all items.
579
+ } @else {
580
+ There are no items available at this time.
581
+ }
582
+ </div>
583
+ </div>
584
+ }
585
+ </div>
586
+ </div>
587
+ </div>
588
+
589
+ @if (menuPosition()) {
590
+ <div
591
+ class="ogrid-datagrid-context-menu-overlay"
592
+ (click)="closeContextMenu()"
593
+ (contextmenu)="$event.preventDefault(); closeContextMenu()"
594
+ >
595
+ <ogrid-grid-context-menu
596
+ [x]="menuPosition()!.x"
597
+ [y]="menuPosition()!.y"
598
+ [hasSelection]="hasCellSelection()"
599
+ [canUndo]="canUndo()"
600
+ [canRedo]="canRedo()"
601
+ (undoAction)="onUndo()"
602
+ (redoAction)="onRedo()"
603
+ (copyAction)="handleCopy()"
604
+ (cutAction)="handleCut()"
605
+ (pasteAction)="handlePaste()"
606
+ (selectAllAction)="handleSelectAllCells()"
607
+ (closeAction)="closeContextMenu()"
608
+ />
609
+ </div>
610
+ }
611
+ </div>
612
+
613
+ @if (statusBarConfig()) {
614
+ <ogrid-status-bar
615
+ [totalCount]="statusBarConfig()!.totalCount"
616
+ [filteredCount]="statusBarConfig()!.filteredCount"
617
+ [selectedCount]="statusBarConfig()!.selectedCount ?? selectedRowIds().size"
618
+ [selectedCellCount]="selectionCellCount()"
619
+ [aggregation]="statusBarConfig()!.aggregation"
620
+ [suppressRowCount]="statusBarConfig()!.suppressRowCount"
621
+ />
622
+ }
623
+
624
+ @if (isLoading()) {
625
+ <div class="ogrid-datagrid-loading-overlay">
626
+ <div class="ogrid-datagrid-loading-inner">
627
+ <div class="ogrid-datagrid-spinner"></div>
628
+ <span>{{ loadingMessage() }}</span>
629
+ </div>
630
+ </div>
631
+ }
632
+ </div>
633
+ `,
634
+ styles: [`
635
+ :host { display: block; }
636
+ .ogrid-datagrid-root { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
637
+ .ogrid-datagrid-wrapper {
638
+ position: relative; flex: 1; min-height: 0; width: 100%; max-width: 100%;
639
+ overflow-x: hidden; overflow-y: auto; background: #fff;
640
+ will-change: scroll-position; outline: none;
641
+ }
642
+ .ogrid-datagrid-wrapper [data-drag-range] { background: rgba(33, 115, 70, 0.12) !important; }
643
+ .ogrid-datagrid-wrapper--fit { width: fit-content; }
644
+ .ogrid-datagrid-wrapper--overflow-x { overflow-x: auto; }
645
+ .ogrid-datagrid-scroll-wrapper { display: flex; flex-direction: column; min-height: 100%; }
646
+ .ogrid-datagrid-table-wrapper--loading { position: relative; opacity: 0.6; }
647
+ .ogrid-datagrid-table {
648
+ width: 100%; border-collapse: collapse; overflow: hidden; table-layout: fixed;
649
+ }
650
+ .ogrid-datagrid-thead {
651
+ position: sticky; top: 0; z-index: 8; background: rgba(0,0,0,0.04);
652
+ }
653
+ .ogrid-datagrid-thead th { background: rgba(0,0,0,0.04); }
654
+ .ogrid-datagrid-header-row { background: rgba(0,0,0,0.04); }
655
+ .ogrid-datagrid-th {
656
+ font-weight: 600; position: relative; padding: 6px 10px; text-align: left;
657
+ font-size: 14px; border-bottom: 1px solid rgba(0,0,0,0.12);
658
+ }
659
+ .ogrid-datagrid-th--pinned-left {
660
+ position: sticky; left: 0; z-index: 9; background: rgba(0,0,0,0.04); will-change: transform;
661
+ }
662
+ .ogrid-datagrid-th--pinned-right {
663
+ position: sticky; right: 0; z-index: 9; background: rgba(0,0,0,0.04); will-change: transform;
664
+ }
665
+ .ogrid-datagrid-group-header {
666
+ text-align: center; font-weight: 600; border-bottom: 2px solid rgba(0,0,0,0.12); padding: 6px;
667
+ }
668
+ .ogrid-datagrid-checkbox-col {
669
+ width: ${CHECKBOX_COLUMN_WIDTH}px; min-width: ${CHECKBOX_COLUMN_WIDTH}px;
670
+ max-width: ${CHECKBOX_COLUMN_WIDTH}px; text-align: center;
671
+ }
672
+ .ogrid-datagrid-checkbox-wrapper { display: flex; align-items: center; justify-content: center; }
673
+ .ogrid-datagrid-row { }
674
+ .ogrid-datagrid-row:hover { background: rgba(0,0,0,0.04); }
675
+ .ogrid-datagrid-row--selected { background: rgba(25,118,210,0.08); }
676
+ .ogrid-datagrid-td { position: relative; padding: 0; height: 1px; border-bottom: 1px solid rgba(0,0,0,0.06); }
677
+ .ogrid-datagrid-td--pinned-left {
678
+ position: sticky; left: 0; z-index: 6; background: #fff; will-change: transform;
679
+ }
680
+ .ogrid-datagrid-td--pinned-right {
681
+ position: sticky; right: 0; z-index: 6; background: #fff; will-change: transform;
682
+ }
683
+ .ogrid-datagrid-cell {
684
+ width: 100%; height: 100%; display: flex; align-items: center; min-width: 0;
685
+ padding: 6px 10px; box-sizing: border-box; overflow: hidden;
686
+ text-overflow: ellipsis; white-space: nowrap; user-select: none; outline: none;
687
+ font-size: 14px;
688
+ }
689
+ .ogrid-datagrid-cell--numeric { justify-content: flex-end; text-align: right; }
690
+ .ogrid-datagrid-cell--boolean { justify-content: center; text-align: center; }
691
+ .ogrid-datagrid-cell--editable { cursor: cell; }
692
+ .ogrid-datagrid-cell--active {
693
+ outline: 2px solid var(--ogrid-selection, #217346); outline-offset: -1px;
694
+ z-index: 2; position: relative; overflow: visible;
695
+ }
696
+ .ogrid-datagrid-cell--in-range { background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)); }
697
+ .ogrid-datagrid-cell--in-cut-range { background: rgba(0,0,0,0.04); opacity: 0.7; }
698
+ .ogrid-datagrid-cell--editing { padding: 0; }
699
+ .ogrid-datagrid-editor-input {
700
+ width: 100%; height: 100%; padding: 6px 10px; border: 2px solid var(--ogrid-selection, #217346);
701
+ box-sizing: border-box; font-size: 14px; outline: none;
702
+ }
703
+ .ogrid-datagrid-editor-select {
704
+ width: 100%; height: 100%; padding: 4px 8px; border: 2px solid var(--ogrid-selection, #217346);
705
+ box-sizing: border-box; font-size: 14px;
706
+ }
707
+ .ogrid-datagrid-fill-handle {
708
+ position: absolute; right: -3px; bottom: -3px; width: 7px; height: 7px;
709
+ background: var(--ogrid-selection, #217346);
710
+ border: 1px solid var(--ogrid-bg, #fff); border-radius: 1px;
711
+ cursor: crosshair; pointer-events: auto; z-index: 3;
712
+ }
713
+ .ogrid-datagrid-resize-handle {
714
+ position: absolute; top: 0; right: -3px; bottom: 0; width: 8px;
715
+ cursor: col-resize; user-select: none;
716
+ }
717
+ .ogrid-datagrid-resize-handle::after {
718
+ content: ''; position: absolute; top: 0; right: 3px; bottom: 0; width: 2px;
719
+ }
720
+ .ogrid-datagrid-resize-handle:hover::after { background: var(--mat-sys-primary, #1976d2); }
721
+ .ogrid-datagrid-resize-handle:active::after { background: var(--mat-sys-primary, #1565c0); }
722
+ .ogrid-datagrid-empty {
723
+ padding: 32px 16px; text-align: center; border-top: 1px solid rgba(0,0,0,0.12);
724
+ background: rgba(0,0,0,0.04);
725
+ }
726
+ .ogrid-datagrid-empty__title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
727
+ .ogrid-datagrid-empty__message { font-size: 14px; color: rgba(0,0,0,0.6); }
728
+ .ogrid-datagrid-empty__clear {
729
+ background: none; border: none; color: var(--mat-sys-primary, #1976d2);
730
+ cursor: pointer; font-size: inherit; text-decoration: underline; padding: 0;
731
+ }
732
+ .ogrid-datagrid-loading-overlay {
733
+ position: absolute; inset: 0; z-index: 2;
734
+ display: flex; align-items: center; justify-content: center;
735
+ background: rgba(255,255,255,0.7);
736
+ }
737
+ .ogrid-datagrid-loading-inner {
738
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
739
+ padding: 16px; background: #fff; border: 1px solid rgba(0,0,0,0.12); border-radius: 4px;
740
+ }
741
+ .ogrid-datagrid-spinner {
742
+ width: 24px; height: 24px; border: 3px solid rgba(0,0,0,0.12);
743
+ border-top-color: var(--mat-sys-primary, #1976d2);
744
+ border-radius: 50%; animation: ogrid-spin 0.8s linear infinite;
745
+ }
746
+ @keyframes ogrid-spin { to { transform: rotate(360deg); } }
747
+ .ogrid-datagrid-context-menu-overlay {
748
+ position: fixed; inset: 0; z-index: 1000;
749
+ }
750
+ `],
751
+ })
752
+ ], DataGridTableComponent);
753
+ export { DataGridTableComponent };