@alaarab/ogrid-angular-radix 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +76 -0
  2. package/dist/esm/column-chooser/column-chooser.component.js +199 -0
  3. package/dist/esm/column-header-filter/column-header-filter.component.js +497 -0
  4. package/dist/esm/datagrid-table/datagrid-table.component.js +573 -0
  5. package/dist/esm/index.js +14 -0
  6. package/dist/esm/ogrid/ogrid.component.js +77 -0
  7. package/dist/esm/pagination-controls/pagination-controls.component.js +189 -0
  8. package/dist/types/column-chooser/column-chooser.component.d.ts +26 -0
  9. package/dist/types/column-header-filter/column-header-filter.component.d.ts +67 -0
  10. package/dist/types/datagrid-table/datagrid-table.component.d.ts +131 -0
  11. package/dist/types/index.d.ts +12 -0
  12. package/dist/types/ogrid/ogrid.component.d.ts +14 -0
  13. package/dist/types/pagination-controls/pagination-controls.component.d.ts +15 -0
  14. package/jest-mocks/angular-cdk-overlay.cjs.js +38 -0
  15. package/jest-mocks/style-mock.js +1 -0
  16. package/jest.config.js +43 -0
  17. package/package.json +37 -0
  18. package/scripts/compile-styles.js +53 -0
  19. package/src/__tests__/column-chooser.component.spec.ts.skip +195 -0
  20. package/src/__tests__/column-header-filter.component.spec.ts.skip +401 -0
  21. package/src/__tests__/datagrid-table.component.spec.ts.skip +417 -0
  22. package/src/__tests__/exports.test.ts +54 -0
  23. package/src/__tests__/ogrid.component.spec.ts.skip +236 -0
  24. package/src/__tests__/pagination-controls.component.spec.ts.skip +190 -0
  25. package/src/column-chooser/column-chooser.component.ts +204 -0
  26. package/src/column-header-filter/column-header-filter.component.ts +528 -0
  27. package/src/datagrid-table/datagrid-table.component.scss +289 -0
  28. package/src/datagrid-table/datagrid-table.component.ts +636 -0
  29. package/src/index.ts +16 -0
  30. package/src/ogrid/ogrid.component.ts +78 -0
  31. package/src/pagination-controls/pagination-controls.component.ts +187 -0
  32. package/tsconfig.build.json +9 -0
  33. package/tsconfig.json +21 -0
@@ -0,0 +1,636 @@
1
+ import {
2
+ Component, input, signal, computed, effect,
3
+ ChangeDetectionStrategy, ElementRef, viewChild,
4
+ } from '@angular/core';
5
+ import {
6
+ DataGridStateService,
7
+ ColumnReorderService,
8
+ VirtualScrollService,
9
+ StatusBarComponent,
10
+ GridContextMenuComponent,
11
+ MarchingAntsOverlayComponent,
12
+ EmptyStateComponent,
13
+ buildHeaderRows,
14
+ getCellValue,
15
+ CHECKBOX_COLUMN_WIDTH,
16
+ DEFAULT_MIN_COLUMN_WIDTH,
17
+ getHeaderFilterConfig,
18
+ getCellRenderDescriptor,
19
+ resolveCellDisplayContent,
20
+ resolveCellStyle,
21
+ } from '@alaarab/ogrid-angular';
22
+ import type {
23
+ IOGridDataGridProps,
24
+ IColumnDef,
25
+ RowId,
26
+ CellRenderDescriptor,
27
+ HeaderFilterConfig,
28
+ } from '@alaarab/ogrid-angular';
29
+ import { ColumnHeaderFilterComponent } from '../column-header-filter/column-header-filter.component';
30
+
31
+ /**
32
+ * DataGridTable component for Angular Radix using native HTML table.
33
+ * Standalone component with lightweight styling and CSS variables.
34
+ */
35
+ @Component({
36
+ selector: 'ogrid-datagrid-table',
37
+ standalone: true,
38
+ imports: [ColumnHeaderFilterComponent, StatusBarComponent, GridContextMenuComponent, MarchingAntsOverlayComponent, EmptyStateComponent],
39
+ providers: [DataGridStateService],
40
+ changeDetection: ChangeDetectionStrategy.OnPush,
41
+ styleUrl: './datagrid-table.component.scss',
42
+ template: `
43
+ <div class="ogrid-datagrid-root">
44
+ <div
45
+ #wrapperEl
46
+ class="ogrid-datagrid-wrapper"
47
+ [class.ogrid-datagrid-wrapper--fit]="layoutModeFit()"
48
+ [class.ogrid-datagrid-wrapper--overflow-x]="allowOverflowX()"
49
+ tabindex="0"
50
+ role="region"
51
+ [attr.aria-label]="ariaLabel()"
52
+ [attr.aria-labelledby]="ariaLabelledBy()"
53
+ (mousedown)="onWrapperMouseDown($event)"
54
+ (keydown)="onGridKeyDown($event)"
55
+ (contextmenu)="$event.preventDefault()"
56
+ [attr.data-overflow-x]="allowOverflowX() ? 'true' : 'false'"
57
+ >
58
+ <div class="ogrid-datagrid-scroll-wrapper">
59
+ <div [style.minWidth.px]="allowOverflowX() ? minTableWidth() : undefined">
60
+ <div [class.ogrid-datagrid-table-wrapper--loading]="isLoading() && items().length > 0" #tableContainerEl>
61
+ <table class="ogrid-datagrid-table" [style.minWidth.px]="minTableWidth()"
62
+ [attr.data-freeze-rows]="freezeRows()"
63
+ [attr.data-freeze-cols]="freezeCols()"
64
+ >
65
+ <thead class="ogrid-datagrid-thead">
66
+ @for (row of headerRows(); track $index; let rowIdx = $index) {
67
+ <tr class="ogrid-datagrid-header-row">
68
+ @if (rowIdx === headerRows().length - 1 && hasCheckboxCol()) {
69
+ <th class="ogrid-datagrid-th ogrid-datagrid-checkbox-col" [attr.rowSpan]="headerRows().length > 1 ? 1 : null">
70
+ <div class="ogrid-datagrid-checkbox-wrapper">
71
+ <input
72
+ type="checkbox"
73
+ [checked]="allSelected()"
74
+ [indeterminate]="someSelected()"
75
+ (change)="onSelectAllChange($event)"
76
+ aria-label="Select all rows"
77
+ />
78
+ </div>
79
+ </th>
80
+ }
81
+ @if (rowIdx === 0 && rowIdx < headerRows().length - 1 && hasCheckboxCol()) {
82
+ <th [attr.rowSpan]="headerRows().length - 1" class="ogrid-datagrid-th" style="width: 48px; min-width: 48px; padding: 0;"></th>
83
+ }
84
+ @for (cell of row; track $index; let cellIdx = $index) {
85
+ @if (cell.isGroup) {
86
+ <th [attr.colSpan]="cell.colSpan" scope="colgroup" class="ogrid-datagrid-th ogrid-datagrid-group-header">
87
+ {{ cell.label }}
88
+ </th>
89
+ } @else {
90
+ @let col = asColumnDef(cell.columnDef);
91
+ @let colIdx = visibleColIndex(col);
92
+ @let isFreezeCol = freezeCols() != null && (freezeCols() ?? 0) >= 1 && colIdx < (freezeCols() ?? 0);
93
+ @let colW = getColumnWidth(col);
94
+ <th scope="col"
95
+ class="ogrid-datagrid-th"
96
+ [class.ogrid-datagrid-th--pinned-left]="col.pinned === 'left' || (isFreezeCol && colIdx === 0)"
97
+ [class.ogrid-datagrid-th--pinned-right]="col.pinned === 'right'"
98
+ [attr.rowSpan]="headerRows().length > 1 ? headerRows().length - rowIdx : null"
99
+ [attr.data-column-id]="col.columnId"
100
+ [style.minWidth.px]="col.minWidth ?? 80"
101
+ [style.width.px]="colW"
102
+ [style.maxWidth.px]="colW"
103
+ [style.cursor]="columnReorderService.isDragging() ? 'grabbing' : 'grab'"
104
+ (mousedown)="onHeaderMouseDown(col.columnId, $event)"
105
+ >
106
+ <column-header-filter
107
+ [columnKey]="col.columnId"
108
+ [columnName]="col.name"
109
+ [filterType]="getFilterConfig(col).filterType"
110
+ [isSorted]="getFilterConfig(col).isSorted"
111
+ [isSortedDescending]="getFilterConfig(col).isSortedDescending"
112
+ [onSort]="getFilterConfig(col).onSort"
113
+ [selectedValues]="getFilterConfig(col).selectedValues"
114
+ [onFilterChange]="getFilterConfig(col).onFilterChange"
115
+ [options]="getFilterConfig(col).options"
116
+ [isLoadingOptions]="getFilterConfig(col).isLoadingOptions ?? false"
117
+ [textValue]="getFilterConfig(col).textValue ?? ''"
118
+ [onTextChange]="getFilterConfig(col).onTextChange"
119
+ [selectedUser]="getFilterConfig(col).selectedUser"
120
+ [onUserChange]="getFilterConfig(col).onUserChange"
121
+ [peopleSearch]="getFilterConfig(col).peopleSearch"
122
+ [dateValue]="getFilterConfig(col).dateValue"
123
+ [onDateChange]="getFilterConfig(col).onDateChange"
124
+ />
125
+ <div class="ogrid-datagrid-resize-handle" (mousedown)="onResizeStart($event, col)"></div>
126
+ </th>
127
+ }
128
+ }
129
+ </tr>
130
+ }
131
+ </thead>
132
+ @if (!showEmptyInGrid()) {
133
+ <tbody>
134
+ @for (item of items(); track getRowId()(item); let rowIndex = $index) {
135
+ @let rowId = getRowId()(item);
136
+ @let isSelected = selectedRowIds().has(rowId);
137
+ <tr
138
+ class="ogrid-datagrid-row"
139
+ [class.ogrid-datagrid-row--selected]="isSelected"
140
+ [attr.data-row-id]="rowId"
141
+ (click)="onRowClick($event, rowId)"
142
+ >
143
+ @if (hasCheckboxCol()) {
144
+ <td class="ogrid-datagrid-td ogrid-datagrid-checkbox-col">
145
+ <div
146
+ class="ogrid-datagrid-checkbox-wrapper"
147
+ [attr.data-row-index]="rowIndex"
148
+ [attr.data-col-index]="0"
149
+ (click)="$event.stopPropagation()"
150
+ >
151
+ <input
152
+ type="checkbox"
153
+ [checked]="isSelected"
154
+ (change)="onRowCheckboxChange(rowId, $event, rowIndex)"
155
+ [attr.aria-label]="'Select row ' + (rowIndex + 1)"
156
+ />
157
+ </div>
158
+ </td>
159
+ }
160
+ @for (colLayout of columnLayouts(); track colLayout.col.columnId; let colIdx = $index) {
161
+ <td
162
+ class="ogrid-datagrid-td"
163
+ [class.ogrid-datagrid-td--pinned-left]="colLayout.pinnedLeft"
164
+ [class.ogrid-datagrid-td--pinned-right]="colLayout.pinnedRight"
165
+ [style.minWidth.px]="colLayout.minWidth"
166
+ [style.width.px]="colLayout.width"
167
+ [style.maxWidth.px]="colLayout.width"
168
+ >
169
+ @let descriptor = getCellDescriptor(item, colLayout.col, rowIndex, colIdx);
170
+ @if (descriptor.mode === 'editing-inline') {
171
+ <div class="ogrid-datagrid-cell ogrid-datagrid-cell--editing">
172
+ @switch (descriptor.editorType) {
173
+ @case ('checkbox') {
174
+ <input
175
+ type="checkbox"
176
+ [checked]="!!descriptor.value"
177
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLInputElement).checked, rowIndex, descriptor.globalColIndex)"
178
+ (keydown)="$event.key === 'Escape' && cancelEdit()"
179
+ />
180
+ }
181
+ @case ('select') {
182
+ <select
183
+ class="ogrid-datagrid-editor-select"
184
+ [value]="descriptor.value != null ? '' + descriptor.value : ''"
185
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLSelectElement).value, rowIndex, descriptor.globalColIndex)"
186
+ (keydown)="$event.key === 'Escape' && cancelEdit()"
187
+ >
188
+ @for (v of getSelectValues(colLayout.col); track v) {
189
+ <option [value]="v">{{ v }}</option>
190
+ }
191
+ </select>
192
+ }
193
+ @case ('date') {
194
+ <input
195
+ type="date"
196
+ class="ogrid-datagrid-editor-input"
197
+ [value]="formatDateForInput(descriptor.value)"
198
+ (change)="commitEdit(item, colLayout.col.columnId, descriptor.value, ($event.target as HTMLInputElement).value, rowIndex, descriptor.globalColIndex)"
199
+ (keydown)="onEditorKeydown($event, item, colLayout.col.columnId, descriptor.value, rowIndex, descriptor.globalColIndex)"
200
+ />
201
+ }
202
+ @default {
203
+ <input
204
+ type="text"
205
+ class="ogrid-datagrid-editor-input"
206
+ [value]="descriptor.value != null ? '' + descriptor.value : ''"
207
+ (keydown)="onEditorKeydown($event, item, colLayout.col.columnId, descriptor.value, rowIndex, descriptor.globalColIndex)"
208
+ />
209
+ }
210
+ }
211
+ </div>
212
+ } @else {
213
+ @let content = resolveCellContent(colLayout.col, item, descriptor.displayValue);
214
+ @let cellStyle = resolveCellStyleFn(colLayout.col, item);
215
+ <div
216
+ class="ogrid-datagrid-cell"
217
+ [class.ogrid-datagrid-cell--active]="descriptor.isActive && !descriptor.isInRange"
218
+ [class.ogrid-datagrid-cell--in-range]="descriptor.isInRange"
219
+ [class.ogrid-datagrid-cell--in-cut-range]="descriptor.isInCutRange"
220
+ [class.ogrid-datagrid-cell--editable]="descriptor.canEditAny"
221
+ [class.ogrid-datagrid-cell--numeric]="colLayout.col.type === 'numeric'"
222
+ [class.ogrid-datagrid-cell--boolean]="colLayout.col.type === 'boolean'"
223
+ [attr.data-row-index]="rowIndex"
224
+ [attr.data-col-index]="descriptor.globalColIndex"
225
+ [attr.data-in-range]="descriptor.isInRange ? 'true' : null"
226
+ [attr.tabindex]="descriptor.isActive ? 0 : -1"
227
+ (mousedown)="onCellMouseDown($event, rowIndex, descriptor.globalColIndex)"
228
+ (click)="onCellClick(rowIndex, descriptor.globalColIndex)"
229
+ (contextmenu)="onCellContextMenu($event)"
230
+ (dblclick)="descriptor.canEditAny ? onCellDblClick(descriptor.rowId, colLayout.col.columnId) : null"
231
+ [attr.role]="descriptor.canEditAny ? 'button' : null"
232
+ [style]="cellStyle ?? undefined"
233
+ >
234
+ {{ content }}
235
+ @if (descriptor.canEditAny && descriptor.isSelectionEndCell) {
236
+ <div
237
+ class="ogrid-datagrid-fill-handle"
238
+ (mousedown)="onFillHandleMouseDown($event)"
239
+ aria-label="Fill handle"
240
+ ></div>
241
+ }
242
+ </div>
243
+ }
244
+ </td>
245
+ }
246
+ </tr>
247
+ }
248
+ </tbody>
249
+ }
250
+ </table>
251
+
252
+ <ogrid-marching-ants-overlay
253
+ [containerEl]="tableContainerEl()"
254
+ [selectionRange]="state().interaction.selectionRange"
255
+ [copyRange]="state().interaction.copyRange"
256
+ [cutRange]="state().interaction.cutRange"
257
+ [colOffset]="state().layout.colOffset"
258
+ [columnSizingVersion]="columnSizingVersion()"
259
+ ></ogrid-marching-ants-overlay>
260
+
261
+ @if (showEmptyInGrid() && emptyState()) {
262
+ <div class="ogrid-datagrid-empty">
263
+ <div class="ogrid-datagrid-empty__title">No results found</div>
264
+ <div class="ogrid-datagrid-empty__message">
265
+ @if (emptyState()!.message != null) {
266
+ {{ emptyState()!.message }}
267
+ } @else if (emptyState()!.hasActiveFilters) {
268
+ No items match your current filters. Try adjusting your search or
269
+ <button class="ogrid-datagrid-empty__clear" (click)="emptyState()!.onClearAll?.()">clear all filters</button>
270
+ to see all items.
271
+ } @else {
272
+ There are no items available at this time.
273
+ }
274
+ </div>
275
+ </div>
276
+ }
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ @if (columnReorderService.isDragging() && columnReorderService.dropIndicatorX() !== null) {
282
+ <div class="ogrid-datagrid-drop-indicator" [style.left.px]="columnReorderService.dropIndicatorX()"></div>
283
+ }
284
+
285
+ @if (menuPosition()) {
286
+ <div
287
+ class="ogrid-datagrid-context-menu-overlay"
288
+ (click)="closeContextMenu()"
289
+ (contextmenu)="$event.preventDefault(); closeContextMenu()"
290
+ >
291
+ <ogrid-grid-context-menu
292
+ [x]="menuPosition()!.x"
293
+ [y]="menuPosition()!.y"
294
+ [hasSelection]="hasCellSelection()"
295
+ [canUndo]="canUndo()"
296
+ [canRedo]="canRedo()"
297
+ (undoAction)="onUndo()"
298
+ (redoAction)="onRedo()"
299
+ (copyAction)="handleCopy()"
300
+ (cutAction)="handleCut()"
301
+ (pasteAction)="handlePaste()"
302
+ (selectAllAction)="handleSelectAllCells()"
303
+ (closeAction)="closeContextMenu()"
304
+ />
305
+ </div>
306
+ }
307
+ </div>
308
+
309
+ @if (statusBarConfig()) {
310
+ <ogrid-status-bar
311
+ [totalCount]="statusBarConfig()!.totalCount"
312
+ [filteredCount]="statusBarConfig()!.filteredCount"
313
+ [selectedCount]="statusBarConfig()!.selectedCount ?? selectedRowIds().size"
314
+ [selectedCellCount]="selectionCellCount()"
315
+ [aggregation]="statusBarConfig()!.aggregation"
316
+ [suppressRowCount]="statusBarConfig()!.suppressRowCount"
317
+ />
318
+ }
319
+
320
+ @if (isLoading()) {
321
+ <div class="ogrid-datagrid-loading-overlay">
322
+ <div class="ogrid-datagrid-loading-inner">
323
+ <div class="ogrid-datagrid-spinner"></div>
324
+ <span>{{ loadingMessage() }}</span>
325
+ </div>
326
+ </div>
327
+ }
328
+ </div>
329
+ `,
330
+ })
331
+ export class DataGridTableComponent<T> {
332
+ readonly propsInput = input.required<IOGridDataGridProps<T>>({ alias: 'props' });
333
+
334
+ private readonly wrapperRef = viewChild<ElementRef<HTMLElement>>('wrapperEl');
335
+ private readonly tableContainerRef = viewChild<ElementRef<HTMLElement>>('tableContainerEl');
336
+ private readonly stateService = new DataGridStateService<T>();
337
+ readonly columnReorderService = new ColumnReorderService<T>();
338
+ readonly virtualScrollService = new VirtualScrollService();
339
+
340
+ private lastMouseShift = false;
341
+ private columnSizingVersion = signal(0);
342
+
343
+ constructor() {
344
+ // Wire props and wrapper element to state service
345
+ effect(() => {
346
+ const p = this.propsInput();
347
+ if (p) this.stateService.props.set(p);
348
+ });
349
+
350
+ effect(() => {
351
+ const el = this.wrapperRef()?.nativeElement;
352
+ if (el) {
353
+ this.stateService.wrapperEl.set(el);
354
+ this.columnReorderService.wrapperEl.set(el);
355
+ }
356
+ });
357
+
358
+ // Wire column reorder service inputs
359
+ effect(() => {
360
+ const p = this.propsInput();
361
+ if (p) {
362
+ const cols = this.visibleCols() as IColumnDef<T>[];
363
+ this.columnReorderService.columns.set(cols);
364
+ this.columnReorderService.columnOrder.set(p.columnOrder);
365
+ this.columnReorderService.onColumnOrderChange.set(p.onColumnOrderChange);
366
+ this.columnReorderService.enabled.set(!!p.onColumnOrderChange);
367
+ }
368
+ });
369
+
370
+ // Wire virtual scroll service inputs
371
+ effect(() => {
372
+ const p = this.propsInput();
373
+ if (p) {
374
+ this.virtualScrollService.totalRows.set(p.items.length);
375
+ }
376
+ });
377
+ }
378
+
379
+ // --- Delegated state ---
380
+
381
+ private readonly state = computed(() => this.stateService.getState());
382
+
383
+ readonly items = computed(() => this.propsInput()?.items ?? []);
384
+ readonly getRowId = computed(() => this.propsInput()?.getRowId ?? ((item: T) => (item as Record<string, unknown>)['id'] as RowId));
385
+ readonly isLoading = computed(() => this.propsInput()?.isLoading ?? false);
386
+ readonly loadingMessage = computed(() => 'Loading\u2026');
387
+ readonly freezeRows = computed(() => this.propsInput()?.freezeRows);
388
+ readonly freezeCols = computed(() => this.propsInput()?.freezeCols);
389
+ readonly layoutModeFit = computed(() => (this.propsInput()?.layoutMode ?? 'fill') === 'content');
390
+ readonly ariaLabel = computed(() => this.propsInput()?.['aria-label'] ?? 'Data grid');
391
+ readonly ariaLabelledBy = computed(() => this.propsInput()?.['aria-labelledby']);
392
+ readonly emptyState = computed(() => this.propsInput()?.emptyState);
393
+
394
+ // State service outputs
395
+ readonly visibleCols = computed(() => this.state().layout.visibleCols);
396
+ readonly hasCheckboxCol = computed(() => this.state().layout.hasCheckboxCol);
397
+ readonly colOffset = computed(() => this.state().layout.colOffset);
398
+ readonly containerWidth = computed(() => this.state().layout.containerWidth);
399
+ readonly minTableWidth = computed(() => this.state().layout.minTableWidth);
400
+ readonly desiredTableWidth = computed(() => this.state().layout.desiredTableWidth);
401
+ readonly columnSizingOverrides = computed(() => this.state().layout.columnSizingOverrides);
402
+
403
+ readonly selectedRowIds = computed(() => this.state().rowSelection.selectedRowIds);
404
+ readonly allSelected = computed(() => this.state().rowSelection.allSelected);
405
+ readonly someSelected = computed(() => this.state().rowSelection.someSelected);
406
+
407
+ readonly editingCell = computed(() => this.state().editing.editingCell);
408
+ readonly pendingEditorValue = computed(() => this.state().editing.pendingEditorValue);
409
+
410
+ readonly activeCell = computed(() => this.state().interaction.activeCell);
411
+ readonly selectionRange = computed(() => this.state().interaction.selectionRange);
412
+ readonly hasCellSelection = computed(() => this.state().interaction.hasCellSelection);
413
+ readonly cutRange = computed(() => this.state().interaction.cutRange);
414
+ readonly copyRange = computed(() => this.state().interaction.copyRange);
415
+ readonly canUndo = computed(() => this.state().interaction.canUndo);
416
+ readonly canRedo = computed(() => this.state().interaction.canRedo);
417
+ readonly isDragging = computed(() => this.state().interaction.isDragging);
418
+
419
+ readonly menuPosition = computed(() => this.state().contextMenu.menuPosition);
420
+ readonly statusBarConfig = computed(() => this.state().viewModels.statusBarConfig);
421
+ readonly showEmptyInGrid = computed(() => this.state().viewModels.showEmptyInGrid);
422
+ readonly headerFilterInput = computed(() => this.state().viewModels.headerFilterInput);
423
+ readonly cellDescriptorInput = computed(() => this.state().viewModels.cellDescriptorInput);
424
+
425
+ readonly allowOverflowX = computed(() => {
426
+ const p = this.propsInput();
427
+ if (p?.suppressHorizontalScroll) return false;
428
+ const cw = this.containerWidth();
429
+ const mtw = this.minTableWidth();
430
+ const dtw = this.desiredTableWidth();
431
+ return cw > 0 && (mtw > cw || dtw > cw);
432
+ });
433
+
434
+ readonly selectionCellCount = computed(() => {
435
+ const sr = this.selectionRange();
436
+ if (!sr) return undefined;
437
+ return (Math.abs(sr.endRow - sr.startRow) + 1) * (Math.abs(sr.endCol - sr.startCol) + 1);
438
+ });
439
+
440
+ // Header rows from column definition
441
+ readonly headerRows = computed(() => {
442
+ const p = this.propsInput();
443
+ if (!p) return [];
444
+ return buildHeaderRows(p.columns, p.visibleColumns);
445
+ });
446
+
447
+ // Pre-computed column layouts
448
+ readonly columnLayouts = computed(() => {
449
+ const cols = this.visibleCols() as IColumnDef<T>[];
450
+ const fc = this.freezeCols();
451
+ return cols.map((col, colIdx) => {
452
+ const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
453
+ const pinnedLeft = col.pinned === 'left' || (isFreezeCol && colIdx === 0);
454
+ const pinnedRight = col.pinned === 'right';
455
+ const w = this.getColumnWidth(col);
456
+ return {
457
+ col,
458
+ pinnedLeft,
459
+ pinnedRight,
460
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
461
+ width: w,
462
+ };
463
+ });
464
+ });
465
+
466
+ // --- Helper methods ---
467
+
468
+ asColumnDef(colDef: unknown): IColumnDef<T> {
469
+ return colDef as IColumnDef<T>;
470
+ }
471
+
472
+ visibleColIndex(col: IColumnDef<T>): number {
473
+ return (this.visibleCols() as IColumnDef<T>[]).indexOf(col);
474
+ }
475
+
476
+ getColumnWidth(col: IColumnDef<T>): number {
477
+ const overrides = this.columnSizingOverrides();
478
+ const override = overrides[col.columnId];
479
+ if (override) return override.widthPx;
480
+ return col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
481
+ }
482
+
483
+ getFilterConfig(col: IColumnDef<T>): HeaderFilterConfig {
484
+ return getHeaderFilterConfig(col, this.headerFilterInput());
485
+ }
486
+
487
+ getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor {
488
+ return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
489
+ }
490
+
491
+ resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): string {
492
+ return resolveCellDisplayContent(col, item, displayValue);
493
+ }
494
+
495
+ resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined {
496
+ return resolveCellStyle(col, item);
497
+ }
498
+
499
+ getSelectValues(col: IColumnDef<T>): string[] {
500
+ const params = col.cellEditorParams;
501
+ if (params && typeof params === 'object' && 'values' in params) {
502
+ return (params as { values: unknown[] }).values.map(String);
503
+ }
504
+ return [];
505
+ }
506
+
507
+ formatDateForInput(value: unknown): string {
508
+ if (!value) return '';
509
+ const d = new Date(String(value));
510
+ if (Number.isNaN(d.getTime())) return '';
511
+ return d.toISOString().split('T')[0];
512
+ }
513
+
514
+ // --- Event handlers ---
515
+
516
+ onWrapperMouseDown(event: MouseEvent): void {
517
+ this.lastMouseShift = event.shiftKey;
518
+ }
519
+
520
+ onGridKeyDown(event: KeyboardEvent): void {
521
+ this.state().interaction.handleGridKeyDown(event);
522
+ }
523
+
524
+ onCellMouseDown(event: MouseEvent, rowIndex: number, globalColIndex: number): void {
525
+ this.state().interaction.handleCellMouseDown(event, rowIndex, globalColIndex);
526
+ }
527
+
528
+ onCellClick(rowIndex: number, globalColIndex: number): void {
529
+ this.state().interaction.setActiveCell({ rowIndex, columnIndex: globalColIndex });
530
+ }
531
+
532
+ onCellContextMenu(event: MouseEvent): void {
533
+ this.state().contextMenu.handleCellContextMenu(event);
534
+ }
535
+
536
+ onCellDblClick(rowId: RowId, columnId: string): void {
537
+ this.state().editing.setEditingCell({ rowId, columnId });
538
+ }
539
+
540
+ onFillHandleMouseDown(event: MouseEvent): void {
541
+ this.state().interaction.handleFillHandleMouseDown(event);
542
+ }
543
+
544
+ onResizeStart(event: MouseEvent, col: IColumnDef<T>): void {
545
+ event.preventDefault();
546
+ const startX = event.clientX;
547
+ const startWidth = this.getColumnWidth(col);
548
+ const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
549
+
550
+ const onMove = (e: MouseEvent) => {
551
+ const delta = e.clientX - startX;
552
+ const newWidth = Math.max(minWidth, startWidth + delta);
553
+ const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
554
+ this.state().layout.setColumnSizingOverrides(overrides);
555
+ this.columnSizingVersion.update(v => v + 1);
556
+ };
557
+
558
+ const onUp = () => {
559
+ window.removeEventListener('mousemove', onMove);
560
+ window.removeEventListener('mouseup', onUp);
561
+ const finalWidth = this.getColumnWidth(col);
562
+ this.state().layout.onColumnResized?.(col.columnId, finalWidth);
563
+ };
564
+
565
+ window.addEventListener('mousemove', onMove);
566
+ window.addEventListener('mouseup', onUp);
567
+ }
568
+
569
+ onSelectAllChange(event: Event): void {
570
+ const checked = (event.target as HTMLInputElement).checked;
571
+ this.state().rowSelection.handleSelectAll(!!checked);
572
+ }
573
+
574
+ onRowClick(event: MouseEvent, rowId: RowId): void {
575
+ const p = this.propsInput();
576
+ if (p?.rowSelection !== 'single') return;
577
+ const ids = this.selectedRowIds();
578
+ this.state().rowSelection.updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
579
+ }
580
+
581
+ onRowCheckboxChange(rowId: RowId, event: Event, rowIndex: number): void {
582
+ const checked = (event.target as HTMLInputElement).checked;
583
+ this.state().rowSelection.handleRowCheckboxChange(rowId, checked, rowIndex, this.lastMouseShift);
584
+ }
585
+
586
+ commitEdit(item: T, columnId: string, oldValue: unknown, newValue: unknown, rowIndex: number, globalColIndex: number): void {
587
+ this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
588
+ }
589
+
590
+ cancelEdit(): void {
591
+ this.state().editing.setEditingCell(null);
592
+ }
593
+
594
+ onEditorKeydown(event: KeyboardEvent, item: T, columnId: string, oldValue: unknown, rowIndex: number, globalColIndex: number): void {
595
+ if (event.key === 'Enter') {
596
+ event.preventDefault();
597
+ const newValue = (event.target as HTMLInputElement).value;
598
+ this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
599
+ } else if (event.key === 'Escape') {
600
+ event.preventDefault();
601
+ this.cancelEdit();
602
+ }
603
+ }
604
+
605
+ closeContextMenu(): void {
606
+ this.state().contextMenu.closeContextMenu();
607
+ }
608
+
609
+ handleCopy(): void {
610
+ this.state().interaction.handleCopy();
611
+ }
612
+
613
+ handleCut(): void {
614
+ this.state().interaction.handleCut();
615
+ }
616
+
617
+ handlePaste(): void {
618
+ void this.state().interaction.handlePaste();
619
+ }
620
+
621
+ handleSelectAllCells(): void {
622
+ this.state().interaction.handleSelectAllCells();
623
+ }
624
+
625
+ onUndo(): void {
626
+ this.state().interaction.onUndo?.();
627
+ }
628
+
629
+ onRedo(): void {
630
+ this.state().interaction.onRedo?.();
631
+ }
632
+
633
+ onHeaderMouseDown(columnId: string, event: MouseEvent): void {
634
+ this.columnReorderService.handleHeaderMouseDown(columnId, event);
635
+ }
636
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @alaarab/ogrid-angular-radix
3
+ *
4
+ * Lightweight Angular data grid using Angular CDK for overlays.
5
+ * This is the recommended "default" option for Angular developers.
6
+ */
7
+
8
+ // Re-export everything from the base Angular package
9
+ export * from '@alaarab/ogrid-angular';
10
+
11
+ // Export our UI components
12
+ export { OGridComponent } from './ogrid/ogrid.component';
13
+ export { DataGridTableComponent } from './datagrid-table/datagrid-table.component';
14
+ export { ColumnHeaderFilterComponent } from './column-header-filter/column-header-filter.component';
15
+ export { ColumnChooserComponent } from './column-chooser/column-chooser.component';
16
+ export { PaginationControlsComponent } from './pagination-controls/pagination-controls.component';