@alaarab/ogrid-angular-material 2.0.2 → 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.
@@ -0,0 +1,122 @@
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, Output, EventEmitter, ViewChild } from '@angular/core';
8
+ import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
9
+ import { MatButtonModule } from '@angular/material/button';
10
+ import { MatIconModule } from '@angular/material/icon';
11
+ import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
12
+ /**
13
+ * Column header dropdown menu for pin/unpin actions.
14
+ * Uses Angular Material MatMenu.
15
+ */
16
+ let ColumnHeaderMenuComponent = class ColumnHeaderMenuComponent {
17
+ constructor() {
18
+ this.canPinLeft = true;
19
+ this.canPinRight = true;
20
+ this.canUnpin = false;
21
+ this.pinLeft = new EventEmitter();
22
+ this.pinRight = new EventEmitter();
23
+ this.unpin = new EventEmitter();
24
+ this.menuItems = COLUMN_HEADER_MENU_ITEMS;
25
+ }
26
+ handlePinLeft() {
27
+ if (this.canPinLeft) {
28
+ this.pinLeft.emit();
29
+ }
30
+ }
31
+ handlePinRight() {
32
+ if (this.canPinRight) {
33
+ this.pinRight.emit();
34
+ }
35
+ }
36
+ handleUnpin() {
37
+ if (this.canUnpin) {
38
+ this.unpin.emit();
39
+ }
40
+ }
41
+ };
42
+ __decorate([
43
+ Input()
44
+ ], ColumnHeaderMenuComponent.prototype, "columnId", void 0);
45
+ __decorate([
46
+ Input()
47
+ ], ColumnHeaderMenuComponent.prototype, "canPinLeft", void 0);
48
+ __decorate([
49
+ Input()
50
+ ], ColumnHeaderMenuComponent.prototype, "canPinRight", void 0);
51
+ __decorate([
52
+ Input()
53
+ ], ColumnHeaderMenuComponent.prototype, "canUnpin", void 0);
54
+ __decorate([
55
+ Output()
56
+ ], ColumnHeaderMenuComponent.prototype, "pinLeft", void 0);
57
+ __decorate([
58
+ Output()
59
+ ], ColumnHeaderMenuComponent.prototype, "pinRight", void 0);
60
+ __decorate([
61
+ Output()
62
+ ], ColumnHeaderMenuComponent.prototype, "unpin", void 0);
63
+ __decorate([
64
+ ViewChild(MatMenuTrigger)
65
+ ], ColumnHeaderMenuComponent.prototype, "menuTrigger", void 0);
66
+ ColumnHeaderMenuComponent = __decorate([
67
+ Component({
68
+ selector: 'column-header-menu',
69
+ standalone: true,
70
+ imports: [MatMenuModule, MatButtonModule, MatIconModule],
71
+ template: `
72
+ <button
73
+ mat-icon-button
74
+ [matMenuTriggerFor]="menu"
75
+ class="column-header-menu-trigger"
76
+ [attr.aria-label]="'Column options for ' + columnId"
77
+ >
78
+ <mat-icon>more_vert</mat-icon>
79
+ </button>
80
+
81
+ <mat-menu #menu="matMenu">
82
+ <button
83
+ mat-menu-item
84
+ [disabled]="!canPinLeft"
85
+ (click)="handlePinLeft()"
86
+ >
87
+ {{ menuItems[0].label }}
88
+ </button>
89
+ <button
90
+ mat-menu-item
91
+ [disabled]="!canPinRight"
92
+ (click)="handlePinRight()"
93
+ >
94
+ {{ menuItems[1].label }}
95
+ </button>
96
+ <button
97
+ mat-menu-item
98
+ [disabled]="!canUnpin"
99
+ (click)="handleUnpin()"
100
+ >
101
+ {{ menuItems[2].label }}
102
+ </button>
103
+ </mat-menu>
104
+ `,
105
+ styles: [`
106
+ .column-header-menu-trigger {
107
+ opacity: 0;
108
+ transition: opacity 0.15s;
109
+ width: 24px;
110
+ height: 24px;
111
+ line-height: 24px;
112
+ padding: 0;
113
+ }
114
+
115
+ :host:hover .column-header-menu-trigger,
116
+ .column-header-menu-trigger:focus {
117
+ opacity: 1;
118
+ }
119
+ `],
120
+ })
121
+ ], ColumnHeaderMenuComponent);
122
+ export { ColumnHeaderMenuComponent };
@@ -4,116 +4,10 @@ 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, computed, effect, ChangeDetectionStrategy, viewChild, } from '@angular/core';
8
- import { DataGridStateService, buildHeaderRows, getCellValue, isInSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-angular';
7
+ import { Component, input, signal, computed, effect, ChangeDetectionStrategy, viewChild, } from '@angular/core';
8
+ import { DataGridStateService, ColumnReorderService, VirtualScrollService, MarchingAntsOverlayComponent, buildHeaderRows, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from '@alaarab/ogrid-angular';
9
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
- }
10
+ import { ColumnHeaderMenuComponent } from '../column-header-menu/column-header-menu.component';
117
11
  /**
118
12
  * DataGridTable component using native HTML table with Material Design-inspired styling.
119
13
  * Standalone component — this is the workhorse of the grid.
@@ -124,7 +18,10 @@ let DataGridTableComponent = class DataGridTableComponent {
124
18
  this.wrapperRef = viewChild('wrapperEl');
125
19
  this.tableContainerRef = viewChild('tableContainerEl');
126
20
  this.stateService = new DataGridStateService();
21
+ this.columnReorderService = new ColumnReorderService();
22
+ this.virtualScrollService = new VirtualScrollService();
127
23
  this.lastMouseShift = false;
24
+ this.columnSizingVersion = signal(0);
128
25
  // --- Delegated state ---
129
26
  this.state = computed(() => this.stateService.getState());
130
27
  this.items = computed(() => this.propsInput()?.items ?? []);
@@ -137,9 +34,13 @@ let DataGridTableComponent = class DataGridTableComponent {
137
34
  this.ariaLabel = computed(() => this.propsInput()?.['aria-label'] ?? 'Data grid');
138
35
  this.ariaLabelledBy = computed(() => this.propsInput()?.['aria-labelledby']);
139
36
  this.emptyState = computed(() => this.propsInput()?.emptyState);
37
+ this.currentPage = computed(() => this.propsInput()?.currentPage ?? 1);
38
+ this.pageSize = computed(() => this.propsInput()?.pageSize ?? 25);
39
+ this.rowNumberOffset = computed(() => this.hasRowNumbersCol() ? (this.currentPage() - 1) * this.pageSize() : 0);
140
40
  // State service outputs
141
41
  this.visibleCols = computed(() => this.state().layout.visibleCols);
142
42
  this.hasCheckboxCol = computed(() => this.state().layout.hasCheckboxCol);
43
+ this.hasRowNumbersCol = computed(() => this.state().layout.hasRowNumbersCol);
143
44
  this.colOffset = computed(() => this.state().layout.colOffset);
144
45
  this.containerWidth = computed(() => this.state().layout.containerWidth);
145
46
  this.minTableWidth = computed(() => this.state().layout.minTableWidth);
@@ -189,10 +90,13 @@ let DataGridTableComponent = class DataGridTableComponent {
189
90
  this.columnLayouts = computed(() => {
190
91
  const cols = this.visibleCols();
191
92
  const fc = this.freezeCols();
93
+ const props = this.propsInput();
94
+ const pinnedCols = props?.pinnedColumns ?? {};
192
95
  return cols.map((col, colIdx) => {
193
96
  const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
194
- const pinnedLeft = col.pinned === 'left' || (isFreezeCol && colIdx === 0);
195
- const pinnedRight = col.pinned === 'right';
97
+ const runtimePinned = pinnedCols[col.columnId];
98
+ const pinnedLeft = runtimePinned === 'left' || (isFreezeCol && colIdx === 0);
99
+ const pinnedRight = runtimePinned === 'right';
196
100
  const w = this.getColumnWidth(col);
197
101
  return {
198
102
  col,
@@ -211,8 +115,28 @@ let DataGridTableComponent = class DataGridTableComponent {
211
115
  });
212
116
  effect(() => {
213
117
  const el = this.wrapperRef()?.nativeElement;
214
- if (el)
118
+ if (el) {
215
119
  this.stateService.wrapperEl.set(el);
120
+ this.columnReorderService.wrapperEl.set(el);
121
+ }
122
+ });
123
+ // Wire column reorder service inputs
124
+ effect(() => {
125
+ const p = this.propsInput();
126
+ if (p) {
127
+ const cols = this.visibleCols();
128
+ this.columnReorderService.columns.set(cols);
129
+ this.columnReorderService.columnOrder.set(p.columnOrder);
130
+ this.columnReorderService.onColumnOrderChange.set(p.onColumnOrderChange);
131
+ this.columnReorderService.enabled.set(!!p.onColumnOrderChange);
132
+ }
133
+ });
134
+ // Wire virtual scroll service inputs
135
+ effect(() => {
136
+ const p = this.propsInput();
137
+ if (p) {
138
+ this.virtualScrollService.totalRows.set(p.items.length);
139
+ }
216
140
  });
217
141
  }
218
142
  // --- Helper methods ---
@@ -288,6 +212,7 @@ let DataGridTableComponent = class DataGridTableComponent {
288
212
  const newWidth = Math.max(minWidth, startWidth + delta);
289
213
  const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
290
214
  this.state().layout.setColumnSizingOverrides(overrides);
215
+ this.columnSizingVersion.update(v => v + 1);
291
216
  };
292
217
  const onUp = () => {
293
218
  window.removeEventListener('mousemove', onMove);
@@ -351,12 +276,36 @@ let DataGridTableComponent = class DataGridTableComponent {
351
276
  onRedo() {
352
277
  this.state().interaction.onRedo?.();
353
278
  }
279
+ onHeaderMouseDown(columnId, event) {
280
+ this.columnReorderService.handleHeaderMouseDown(columnId, event);
281
+ }
282
+ // --- Column pinning methods ---
283
+ onPinColumn(columnId, side) {
284
+ const props = this.propsInput();
285
+ props?.onColumnPinned?.(columnId, side);
286
+ }
287
+ onUnpinColumn(columnId) {
288
+ const props = this.propsInput();
289
+ props?.onColumnPinned?.(columnId, null);
290
+ }
291
+ isPinned(columnId) {
292
+ const props = this.propsInput();
293
+ return props?.pinnedColumns?.[columnId];
294
+ }
295
+ getPinState(columnId) {
296
+ const pinned = this.isPinned(columnId);
297
+ return {
298
+ canPinLeft: pinned !== 'left',
299
+ canPinRight: pinned !== 'right',
300
+ canUnpin: !!pinned,
301
+ };
302
+ }
354
303
  };
355
304
  DataGridTableComponent = __decorate([
356
305
  Component({
357
306
  selector: 'ogrid-datagrid-table',
358
307
  standalone: true,
359
- imports: [ColumnHeaderFilterComponent],
308
+ imports: [ColumnHeaderFilterComponent, ColumnHeaderMenuComponent, MarchingAntsOverlayComponent],
360
309
  providers: [DataGridStateService],
361
310
  changeDetection: ChangeDetectionStrategy.OnPush,
362
311
  template: `
@@ -401,6 +350,14 @@ DataGridTableComponent = __decorate([
401
350
  @if (rowIdx === 0 && rowIdx < headerRows().length - 1 && hasCheckboxCol()) {
402
351
  <th [attr.rowSpan]="headerRows().length - 1" class="ogrid-datagrid-th" style="width: 48px; min-width: 48px; padding: 0;"></th>
403
352
  }
353
+ @if (rowIdx === headerRows().length - 1 && hasRowNumbersCol()) {
354
+ <th class="ogrid-datagrid-th ogrid-row-number-header" [attr.rowSpan]="headerRows().length > 1 ? 1 : null">
355
+ <div class="ogrid-row-number-header-content">#</div>
356
+ </th>
357
+ }
358
+ @if (rowIdx === 0 && rowIdx < headerRows().length - 1 && hasRowNumbersCol()) {
359
+ <th [attr.rowSpan]="headerRows().length - 1" class="ogrid-datagrid-th" [style.width.px]="50" [style.min-width.px]="50" style="padding: 0;"></th>
360
+ }
404
361
  @for (cell of row; track $index; let cellIdx = $index) {
405
362
  @if (cell.isGroup) {
406
363
  <th [attr.colSpan]="cell.colSpan" scope="colgroup" class="ogrid-datagrid-th ogrid-datagrid-group-header">
@@ -411,34 +368,52 @@ DataGridTableComponent = __decorate([
411
368
  @let colIdx = visibleColIndex(col);
412
369
  @let isFreezeCol = freezeCols() != null && (freezeCols() ?? 0) >= 1 && colIdx < (freezeCols() ?? 0);
413
370
  @let colW = getColumnWidth(col);
371
+ @let pinned = isPinned(col.columnId);
372
+ @let pinnedLeft = pinned === 'left' || (isFreezeCol && colIdx === 0);
373
+ @let pinnedRight = pinned === 'right';
414
374
  <th scope="col"
415
375
  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'"
376
+ [class.ogrid-datagrid-th--pinned-left]="pinnedLeft"
377
+ [class.ogrid-datagrid-th--pinned-right]="pinnedRight"
418
378
  [attr.rowSpan]="headerRows().length > 1 ? headerRows().length - rowIdx : null"
379
+ [attr.data-column-id]="col.columnId"
419
380
  [style.minWidth.px]="col.minWidth ?? 80"
420
381
  [style.width.px]="colW"
421
382
  [style.maxWidth.px]="colW"
383
+ [style.cursor]="columnReorderService.isDragging() ? 'grabbing' : 'grab'"
384
+ (mousedown)="onHeaderMouseDown(col.columnId, $event)"
422
385
  >
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
- />
386
+ <div style="display:flex;align-items:center;gap:4px;">
387
+ <ogrid-column-header-filter
388
+ [columnKey]="col.columnId"
389
+ [columnName]="col.name"
390
+ [filterType]="getFilterConfig(col).filterType"
391
+ [isSorted]="getFilterConfig(col).isSorted"
392
+ [isSortedDescending]="getFilterConfig(col).isSortedDescending"
393
+ [onSort]="getFilterConfig(col).onSort"
394
+ [selectedValues]="getFilterConfig(col).selectedValues"
395
+ [onFilterChange]="getFilterConfig(col).onFilterChange"
396
+ [options]="getFilterConfig(col).options"
397
+ [isLoadingOptions]="getFilterConfig(col).isLoadingOptions ?? false"
398
+ [textValue]="getFilterConfig(col).textValue ?? ''"
399
+ [onTextChange]="getFilterConfig(col).onTextChange"
400
+ [selectedUser]="getFilterConfig(col).selectedUser"
401
+ [onUserChange]="getFilterConfig(col).onUserChange"
402
+ [peopleSearch]="getFilterConfig(col).peopleSearch"
403
+ [dateValue]="getFilterConfig(col).dateValue"
404
+ [onDateChange]="getFilterConfig(col).onDateChange"
405
+ />
406
+ @let pinState = getPinState(col.columnId);
407
+ <column-header-menu
408
+ [columnId]="col.columnId"
409
+ [onPinLeft]="() => onPinColumn(col.columnId, 'left')"
410
+ [onPinRight]="() => onPinColumn(col.columnId, 'right')"
411
+ [onUnpin]="() => onUnpinColumn(col.columnId)"
412
+ [canPinLeft]="pinState.canPinLeft"
413
+ [canPinRight]="pinState.canPinRight"
414
+ [canUnpin]="pinState.canUnpin"
415
+ />
416
+ </div>
442
417
  <div class="ogrid-datagrid-resize-handle" (mousedown)="onResizeStart($event, col)"></div>
443
418
  </th>
444
419
  }
@@ -474,6 +449,13 @@ DataGridTableComponent = __decorate([
474
449
  </div>
475
450
  </td>
476
451
  }
452
+ @if (hasRowNumbersCol()) {
453
+ <td class="ogrid-datagrid-td ogrid-row-number-cell">
454
+ <div class="ogrid-row-number-cell-content">
455
+ {{ rowNumberOffset() + rowIndex + 1 }}
456
+ </div>
457
+ </td>
458
+ }
477
459
  @for (colLayout of columnLayouts(); track colLayout.col.columnId; let colIdx = $index) {
478
460
  <td
479
461
  class="ogrid-datagrid-td"
@@ -566,6 +548,15 @@ DataGridTableComponent = __decorate([
566
548
  }
567
549
  </table>
568
550
 
551
+ <ogrid-marching-ants-overlay
552
+ [containerEl]="tableContainerEl()"
553
+ [selectionRange]="state().interaction.selectionRange"
554
+ [copyRange]="state().interaction.copyRange"
555
+ [cutRange]="state().interaction.cutRange"
556
+ [colOffset]="state().layout.colOffset"
557
+ [columnSizingVersion]="columnSizingVersion()"
558
+ ></ogrid-marching-ants-overlay>
559
+
569
560
  @if (showEmptyInGrid() && emptyState()) {
570
561
  <div class="ogrid-datagrid-empty">
571
562
  <div class="ogrid-datagrid-empty__title">No results found</div>
@@ -586,6 +577,10 @@ DataGridTableComponent = __decorate([
586
577
  </div>
587
578
  </div>
588
579
 
580
+ @if (columnReorderService.isDragging() && columnReorderService.dropIndicatorX() !== null) {
581
+ <div class="ogrid-datagrid-drop-indicator" [style.left.px]="columnReorderService.dropIndicatorX()"></div>
582
+ }
583
+
589
584
  @if (menuPosition()) {
590
585
  <div
591
586
  class="ogrid-datagrid-context-menu-overlay"
@@ -658,9 +653,11 @@ DataGridTableComponent = __decorate([
658
653
  }
659
654
  .ogrid-datagrid-th--pinned-left {
660
655
  position: sticky; left: 0; z-index: 9; background: rgba(0,0,0,0.04); will-change: transform;
656
+ border-left: 2px solid var(--mat-sys-primary, #1976d2);
661
657
  }
662
658
  .ogrid-datagrid-th--pinned-right {
663
659
  position: sticky; right: 0; z-index: 9; background: rgba(0,0,0,0.04); will-change: transform;
660
+ border-right: 2px solid var(--mat-sys-primary, #1976d2);
664
661
  }
665
662
  .ogrid-datagrid-group-header {
666
663
  text-align: center; font-weight: 600; border-bottom: 2px solid rgba(0,0,0,0.12); padding: 6px;
@@ -670,15 +667,28 @@ DataGridTableComponent = __decorate([
670
667
  max-width: ${CHECKBOX_COLUMN_WIDTH}px; text-align: center;
671
668
  }
672
669
  .ogrid-datagrid-checkbox-wrapper { display: flex; align-items: center; justify-content: center; }
670
+ .ogrid-row-number-header, .ogrid-row-number-cell {
671
+ width: ${ROW_NUMBER_COLUMN_WIDTH}px; min-width: ${ROW_NUMBER_COLUMN_WIDTH}px;
672
+ max-width: ${ROW_NUMBER_COLUMN_WIDTH}px; text-align: center;
673
+ background: rgba(0,0,0,0.04); font-weight: 600;
674
+ font-variant-numeric: tabular-nums; color: rgba(0,0,0,0.6);
675
+ position: sticky; left: 0; z-index: 3;
676
+ }
677
+ .ogrid-row-number-header { z-index: 4; }
678
+ .ogrid-row-number-header-content, .ogrid-row-number-cell-content {
679
+ display: flex; align-items: center; justify-content: center;
680
+ }
673
681
  .ogrid-datagrid-row { }
674
682
  .ogrid-datagrid-row:hover { background: rgba(0,0,0,0.04); }
675
683
  .ogrid-datagrid-row--selected { background: rgba(25,118,210,0.08); }
676
684
  .ogrid-datagrid-td { position: relative; padding: 0; height: 1px; border-bottom: 1px solid rgba(0,0,0,0.06); }
677
685
  .ogrid-datagrid-td--pinned-left {
678
686
  position: sticky; left: 0; z-index: 6; background: #fff; will-change: transform;
687
+ border-left: 2px solid var(--mat-sys-primary, #1976d2);
679
688
  }
680
689
  .ogrid-datagrid-td--pinned-right {
681
690
  position: sticky; right: 0; z-index: 6; background: #fff; will-change: transform;
691
+ border-right: 2px solid var(--mat-sys-primary, #1976d2);
682
692
  }
683
693
  .ogrid-datagrid-cell {
684
694
  width: 100%; height: 100%; display: flex; align-items: center; min-width: 0;
@@ -698,7 +708,10 @@ DataGridTableComponent = __decorate([
698
708
  .ogrid-datagrid-cell--editing { padding: 0; }
699
709
  .ogrid-datagrid-editor-input {
700
710
  width: 100%; height: 100%; padding: 6px 10px; border: 2px solid var(--ogrid-selection, #217346);
701
- box-sizing: border-box; font-size: 14px; outline: none;
711
+ box-sizing: border-box; font-size: 14px; outline: none; font-family: inherit; line-height: inherit;
712
+ }
713
+ .ogrid-datagrid-cell--numeric .ogrid-datagrid-editor-input {
714
+ text-align: right;
702
715
  }
703
716
  .ogrid-datagrid-editor-select {
704
717
  width: 100%; height: 100%; padding: 4px 8px; border: 2px solid var(--ogrid-selection, #217346);
@@ -744,6 +757,11 @@ DataGridTableComponent = __decorate([
744
757
  border-radius: 50%; animation: ogrid-spin 0.8s linear infinite;
745
758
  }
746
759
  @keyframes ogrid-spin { to { transform: rotate(360deg); } }
760
+ .ogrid-datagrid-drop-indicator {
761
+ position: absolute; top: 0; bottom: 0; width: 3px;
762
+ background: var(--ogrid-primary, #217346);
763
+ pointer-events: none; z-index: 100; transition: left 0.05s;
764
+ }
747
765
  .ogrid-datagrid-context-menu-overlay {
748
766
  position: fixed; inset: 0; z-index: 1000;
749
767
  }
package/dist/esm/index.js CHANGED
@@ -6,3 +6,4 @@ export { DataGridTableComponent } from './datagrid-table/datagrid-table.componen
6
6
  export { ColumnHeaderFilterComponent } from './column-header-filter/column-header-filter.component';
7
7
  export { ColumnChooserComponent } from './column-chooser/column-chooser.component';
8
8
  export { PaginationControlsComponent } from './pagination-controls/pagination-controls.component';
9
+ export { ColumnHeaderMenuComponent } from './column-header-menu/column-header-menu.component';
@@ -0,0 +1,20 @@
1
+ import { EventEmitter } from '@angular/core';
2
+ import { MatMenuTrigger } from '@angular/material/menu';
3
+ /**
4
+ * Column header dropdown menu for pin/unpin actions.
5
+ * Uses Angular Material MatMenu.
6
+ */
7
+ export declare class ColumnHeaderMenuComponent {
8
+ columnId: string;
9
+ canPinLeft: boolean;
10
+ canPinRight: boolean;
11
+ canUnpin: boolean;
12
+ pinLeft: EventEmitter<void>;
13
+ pinRight: EventEmitter<void>;
14
+ unpin: EventEmitter<void>;
15
+ menuTrigger?: MatMenuTrigger;
16
+ readonly menuItems: import("@alaarab/ogrid-core").IColumnHeaderMenuItem[];
17
+ handlePinLeft(): void;
18
+ handlePinRight(): void;
19
+ handleUnpin(): void;
20
+ }
@@ -1,38 +1,5 @@
1
- import type { IOGridDataGridProps, IColumnDef, RowId, ColumnFilterType, IDateFilterValue, UserLike, IFilters, FilterValue } from '@alaarab/ogrid-angular';
2
- type CellRenderMode = 'editing-inline' | 'editing-popover' | 'display';
3
- interface CellRenderDescriptor {
4
- mode: CellRenderMode;
5
- editorType?: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
6
- value?: unknown;
7
- isActive: boolean;
8
- isInRange: boolean;
9
- isInCutRange: boolean;
10
- isSelectionEndCell: boolean;
11
- canEditAny: boolean;
12
- globalColIndex: number;
13
- rowId: RowId;
14
- rowIndex: number;
15
- displayValue?: unknown;
16
- }
17
- interface HeaderFilterConfig {
18
- columnKey: string;
19
- columnName: string;
20
- filterType: ColumnFilterType;
21
- isSorted: boolean;
22
- isSortedDescending: boolean;
23
- onSort: (() => void) | undefined;
24
- selectedValues?: string[];
25
- onFilterChange?: (values: string[]) => void;
26
- options?: string[];
27
- isLoadingOptions?: boolean;
28
- textValue?: string;
29
- onTextChange?: (value: string) => void;
30
- selectedUser?: UserLike;
31
- onUserChange?: (user: UserLike | undefined) => void;
32
- peopleSearch?: (query: string) => Promise<UserLike[]>;
33
- dateValue?: IDateFilterValue;
34
- onDateChange?: (value: IDateFilterValue | undefined) => void;
35
- }
1
+ import { ColumnReorderService, VirtualScrollService } from '@alaarab/ogrid-angular';
2
+ import type { IOGridDataGridProps, IColumnDef, RowId, CellRenderDescriptor, HeaderFilterConfig } from '@alaarab/ogrid-angular';
36
3
  /**
37
4
  * DataGridTable component using native HTML table with Material Design-inspired styling.
38
5
  * Standalone component — this is the workhorse of the grid.
@@ -42,7 +9,10 @@ export declare class DataGridTableComponent<T> {
42
9
  private readonly wrapperRef;
43
10
  private readonly tableContainerRef;
44
11
  private readonly stateService;
12
+ readonly columnReorderService: ColumnReorderService<T>;
13
+ readonly virtualScrollService: VirtualScrollService;
45
14
  private lastMouseShift;
15
+ private columnSizingVersion;
46
16
  constructor();
47
17
  private readonly state;
48
18
  readonly items: import("@angular/core").Signal<T[]>;
@@ -60,8 +30,12 @@ export declare class DataGridTableComponent<T> {
60
30
  message?: string;
61
31
  render?: import("@angular/core").TemplateRef<unknown>;
62
32
  } | undefined>;
33
+ readonly currentPage: import("@angular/core").Signal<number>;
34
+ readonly pageSize: import("@angular/core").Signal<number>;
35
+ readonly rowNumberOffset: import("@angular/core").Signal<number>;
63
36
  readonly visibleCols: import("@angular/core").Signal<IColumnDef<T>[]>;
64
37
  readonly hasCheckboxCol: import("@angular/core").Signal<boolean>;
38
+ readonly hasRowNumbersCol: import("@angular/core").Signal<boolean>;
65
39
  readonly colOffset: import("@angular/core").Signal<number>;
66
40
  readonly containerWidth: import("@angular/core").Signal<number>;
67
41
  readonly minTableWidth: import("@angular/core").Signal<number>;
@@ -77,11 +51,11 @@ export declare class DataGridTableComponent<T> {
77
51
  columnId: string;
78
52
  } | null>;
79
53
  readonly pendingEditorValue: import("@angular/core").Signal<unknown>;
80
- readonly activeCell: import("@angular/core").Signal<import("@alaarab/ogrid-angular").IActiveCell | null>;
81
- readonly selectionRange: import("@angular/core").Signal<import("@alaarab/ogrid-angular").ISelectionRange | null>;
54
+ readonly activeCell: import("@angular/core").Signal<import("@alaarab/ogrid-core").IActiveCell | null>;
55
+ readonly selectionRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
82
56
  readonly hasCellSelection: import("@angular/core").Signal<boolean>;
83
- readonly cutRange: import("@angular/core").Signal<import("@alaarab/ogrid-angular").ISelectionRange | null>;
84
- readonly copyRange: import("@angular/core").Signal<import("@alaarab/ogrid-angular").ISelectionRange | null>;
57
+ readonly cutRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
58
+ readonly copyRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
85
59
  readonly canUndo: import("@angular/core").Signal<boolean>;
86
60
  readonly canRedo: import("@angular/core").Signal<boolean>;
87
61
  readonly isDragging: import("@angular/core").Signal<boolean>;
@@ -89,37 +63,37 @@ export declare class DataGridTableComponent<T> {
89
63
  x: number;
90
64
  y: number;
91
65
  } | null>;
92
- readonly statusBarConfig: import("@angular/core").Signal<import("@alaarab/ogrid-angular").IStatusBarProps | null>;
66
+ readonly statusBarConfig: import("@angular/core").Signal<import("@alaarab/ogrid-core").IStatusBarProps | null>;
93
67
  readonly showEmptyInGrid: import("@angular/core").Signal<boolean>;
94
68
  readonly headerFilterInput: import("@angular/core").Signal<{
95
69
  sortBy?: string;
96
70
  sortDirection: "asc" | "desc";
97
71
  onColumnSort: (columnKey: string) => void;
98
- filters: IFilters;
99
- onFilterChange: (key: string, value: FilterValue | undefined) => void;
72
+ filters: import("@alaarab/ogrid-core").IFilters;
73
+ onFilterChange: (key: string, value: import("@alaarab/ogrid-core").FilterValue | undefined) => void;
100
74
  filterOptions: Record<string, string[]>;
101
75
  loadingFilterOptions: Record<string, boolean>;
102
- peopleSearch?: (query: string) => Promise<UserLike[]>;
76
+ peopleSearch?: (query: string) => Promise<import("@alaarab/ogrid-core").UserLike[]>;
103
77
  }>;
104
78
  readonly cellDescriptorInput: import("@angular/core").Signal<{
105
79
  editingCell: {
106
80
  rowId: RowId;
107
81
  columnId: string;
108
82
  } | null;
109
- activeCell: import("@alaarab/ogrid-angular").IActiveCell | null;
110
- selectionRange: import("@alaarab/ogrid-angular").ISelectionRange | null;
111
- cutRange: import("@alaarab/ogrid-angular").ISelectionRange | null;
112
- copyRange: import("@alaarab/ogrid-angular").ISelectionRange | null;
83
+ activeCell: import("@alaarab/ogrid-core").IActiveCell | null;
84
+ selectionRange: import("@alaarab/ogrid-core").ISelectionRange | null;
85
+ cutRange: import("@alaarab/ogrid-core").ISelectionRange | null;
86
+ copyRange: import("@alaarab/ogrid-core").ISelectionRange | null;
113
87
  colOffset: number;
114
88
  itemsLength: number;
115
89
  getRowId: (item: T) => RowId;
116
90
  editable?: boolean;
117
- onCellValueChanged?: ((event: import("@alaarab/ogrid-angular").ICellValueChangedEvent<T>) => void) | undefined;
91
+ onCellValueChanged?: ((event: import("@alaarab/ogrid-core").ICellValueChangedEvent<T>) => void) | undefined;
118
92
  isDragging: boolean;
119
93
  }>;
120
94
  readonly allowOverflowX: import("@angular/core").Signal<boolean>;
121
95
  readonly selectionCellCount: import("@angular/core").Signal<number | undefined>;
122
- readonly headerRows: import("@angular/core").Signal<import("@alaarab/ogrid-angular").HeaderRow<T>[]>;
96
+ readonly headerRows: import("@angular/core").Signal<import("@alaarab/ogrid-core").HeaderRow<T>[]>;
123
97
  readonly columnLayouts: import("@angular/core").Signal<{
124
98
  col: IColumnDef<T>;
125
99
  pinnedLeft: boolean;
@@ -157,5 +131,13 @@ export declare class DataGridTableComponent<T> {
157
131
  handleSelectAllCells(): void;
158
132
  onUndo(): void;
159
133
  onRedo(): void;
134
+ onHeaderMouseDown(columnId: string, event: MouseEvent): void;
135
+ onPinColumn(columnId: string, side: 'left' | 'right'): void;
136
+ onUnpinColumn(columnId: string): void;
137
+ isPinned(columnId: string): 'left' | 'right' | undefined;
138
+ getPinState(columnId: string): {
139
+ canPinLeft: boolean;
140
+ canPinRight: boolean;
141
+ canUnpin: boolean;
142
+ };
160
143
  }
161
- export {};
@@ -6,3 +6,4 @@ export type { IColumnHeaderFilterProps } from './column-header-filter/column-hea
6
6
  export { ColumnChooserComponent } from './column-chooser/column-chooser.component';
7
7
  export type { IColumnChooserProps } from './column-chooser/column-chooser.component';
8
8
  export { PaginationControlsComponent } from './pagination-controls/pagination-controls.component';
9
+ export { ColumnHeaderMenuComponent } from './column-header-menu/column-header-menu.component';
@@ -10,6 +10,6 @@ export declare class PaginationControlsComponent {
10
10
  readonly entityLabelPlural: import("@angular/core").InputSignal<string>;
11
11
  readonly pageChange: import("@angular/core").OutputEmitterRef<number>;
12
12
  readonly pageSizeChange: import("@angular/core").OutputEmitterRef<number>;
13
- readonly vm: import("@angular/core").Signal<import("@alaarab/ogrid-angular").PaginationViewModel | null>;
13
+ readonly vm: import("@angular/core").Signal<import("@alaarab/ogrid-core").PaginationViewModel | null>;
14
14
  onPageSizeSelect(event: Event): void;
15
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular-material",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "OGrid Angular Material – MatTable-based data grid with sorting, filtering, pagination, column chooser, and CSV export.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -22,7 +22,7 @@
22
22
  "files": ["dist", "README.md", "LICENSE"],
23
23
  "engines": { "node": ">=18" },
24
24
  "dependencies": {
25
- "@alaarab/ogrid-angular": "2.0.2"
25
+ "@alaarab/ogrid-angular": "2.0.4"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@angular/core": "^21.0.0",