@alaarab/ogrid-angular 2.0.4 → 2.0.6

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,309 @@
1
+ import { signal, computed, effect } from '@angular/core';
2
+ import { DataGridStateService } from '../services/datagrid-state.service';
3
+ import { ColumnReorderService } from '../services/column-reorder.service';
4
+ import { VirtualScrollService } from '../services/virtual-scroll.service';
5
+ import { buildHeaderRows, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-core';
6
+ import { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from '../utils';
7
+ /**
8
+ * Abstract base class containing all shared TypeScript logic for DataGridTable components.
9
+ * Framework-specific UI packages extend this with their templates and style overrides.
10
+ *
11
+ * Subclasses must:
12
+ * 1. Provide a @Component decorator with template and styles
13
+ * 2. Call `initBase()` in the constructor (effects require injection context)
14
+ * 3. Implement abstract accessors for propsInput, wrapperRef, and tableContainerRef
15
+ */
16
+ export class BaseDataGridTableComponent {
17
+ constructor() {
18
+ this.stateService = new DataGridStateService();
19
+ this.columnReorderService = new ColumnReorderService();
20
+ this.virtualScrollService = new VirtualScrollService();
21
+ this.lastMouseShift = false;
22
+ this.columnSizingVersion = signal(0);
23
+ // --- Delegated state ---
24
+ this.state = computed(() => this.stateService.getState());
25
+ this.tableContainerEl = computed(() => this.getTableContainerRef()?.nativeElement ?? null);
26
+ this.items = computed(() => this.getProps()?.items ?? []);
27
+ this.getRowId = computed(() => this.getProps()?.getRowId ?? ((item) => item['id']));
28
+ this.isLoading = computed(() => this.getProps()?.isLoading ?? false);
29
+ this.loadingMessage = computed(() => 'Loading\u2026');
30
+ this.freezeRows = computed(() => this.getProps()?.freezeRows);
31
+ this.freezeCols = computed(() => this.getProps()?.freezeCols);
32
+ this.layoutModeFit = computed(() => (this.getProps()?.layoutMode ?? 'fill') === 'content');
33
+ this.ariaLabel = computed(() => this.getProps()?.['aria-label'] ?? 'Data grid');
34
+ this.ariaLabelledBy = computed(() => this.getProps()?.['aria-labelledby']);
35
+ this.emptyState = computed(() => this.getProps()?.emptyState);
36
+ this.currentPage = computed(() => this.getProps()?.currentPage ?? 1);
37
+ this.pageSize = computed(() => this.getProps()?.pageSize ?? 25);
38
+ this.rowNumberOffset = computed(() => this.hasRowNumbersCol() ? (this.currentPage() - 1) * this.pageSize() : 0);
39
+ // State service outputs
40
+ this.visibleCols = computed(() => this.state().layout.visibleCols);
41
+ this.hasCheckboxCol = computed(() => this.state().layout.hasCheckboxCol);
42
+ this.hasRowNumbersCol = computed(() => this.state().layout.hasRowNumbersCol);
43
+ this.colOffset = computed(() => this.state().layout.colOffset);
44
+ this.containerWidth = computed(() => this.state().layout.containerWidth);
45
+ this.minTableWidth = computed(() => this.state().layout.minTableWidth);
46
+ this.desiredTableWidth = computed(() => this.state().layout.desiredTableWidth);
47
+ this.columnSizingOverrides = computed(() => this.state().layout.columnSizingOverrides);
48
+ this.selectedRowIds = computed(() => this.state().rowSelection.selectedRowIds);
49
+ this.allSelected = computed(() => this.state().rowSelection.allSelected);
50
+ this.someSelected = computed(() => this.state().rowSelection.someSelected);
51
+ this.editingCell = computed(() => this.state().editing.editingCell);
52
+ this.pendingEditorValue = computed(() => this.state().editing.pendingEditorValue);
53
+ this.activeCell = computed(() => this.state().interaction.activeCell);
54
+ this.selectionRange = computed(() => this.state().interaction.selectionRange);
55
+ this.hasCellSelection = computed(() => this.state().interaction.hasCellSelection);
56
+ this.cutRange = computed(() => this.state().interaction.cutRange);
57
+ this.copyRange = computed(() => this.state().interaction.copyRange);
58
+ this.canUndo = computed(() => this.state().interaction.canUndo);
59
+ this.canRedo = computed(() => this.state().interaction.canRedo);
60
+ this.isDragging = computed(() => this.state().interaction.isDragging);
61
+ this.menuPosition = computed(() => this.state().contextMenu.menuPosition);
62
+ this.statusBarConfig = computed(() => this.state().viewModels.statusBarConfig);
63
+ this.showEmptyInGrid = computed(() => this.state().viewModels.showEmptyInGrid);
64
+ this.headerFilterInput = computed(() => this.state().viewModels.headerFilterInput);
65
+ this.cellDescriptorInput = computed(() => this.state().viewModels.cellDescriptorInput);
66
+ this.allowOverflowX = computed(() => {
67
+ const p = this.getProps();
68
+ if (p?.suppressHorizontalScroll)
69
+ return false;
70
+ const cw = this.containerWidth();
71
+ const mtw = this.minTableWidth();
72
+ const dtw = this.desiredTableWidth();
73
+ return cw > 0 && (mtw > cw || dtw > cw);
74
+ });
75
+ this.selectionCellCount = computed(() => {
76
+ const sr = this.selectionRange();
77
+ if (!sr)
78
+ return undefined;
79
+ return (Math.abs(sr.endRow - sr.startRow) + 1) * (Math.abs(sr.endCol - sr.startCol) + 1);
80
+ });
81
+ // Header rows from column definition
82
+ this.headerRows = computed(() => {
83
+ const p = this.getProps();
84
+ if (!p)
85
+ return [];
86
+ return buildHeaderRows(p.columns, p.visibleColumns);
87
+ });
88
+ // Pre-computed column layouts
89
+ this.columnLayouts = computed(() => {
90
+ const cols = this.visibleCols();
91
+ const fc = this.freezeCols();
92
+ const props = this.getProps();
93
+ const pinnedCols = props?.pinnedColumns ?? {};
94
+ return cols.map((col, colIdx) => {
95
+ const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
96
+ const runtimePinned = pinnedCols[col.columnId];
97
+ const pinnedLeft = runtimePinned === 'left' || col.pinned === 'left' || (isFreezeCol && colIdx === 0);
98
+ const pinnedRight = runtimePinned === 'right' || col.pinned === 'right';
99
+ const w = this.getColumnWidth(col);
100
+ return {
101
+ col,
102
+ pinnedLeft,
103
+ pinnedRight,
104
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
105
+ width: w,
106
+ };
107
+ });
108
+ });
109
+ }
110
+ /**
111
+ * Initialize base wiring effects. Must be called from subclass constructor
112
+ * (effects need to run inside an injection context).
113
+ */
114
+ initBase() {
115
+ // Wire props to state service
116
+ effect(() => {
117
+ const p = this.getProps();
118
+ if (p)
119
+ this.stateService.props.set(p);
120
+ });
121
+ // Wire wrapper element
122
+ effect(() => {
123
+ const el = this.getWrapperRef()?.nativeElement;
124
+ if (el) {
125
+ this.stateService.wrapperEl.set(el);
126
+ this.columnReorderService.wrapperEl.set(el);
127
+ }
128
+ });
129
+ // Wire column reorder service inputs
130
+ effect(() => {
131
+ const p = this.getProps();
132
+ if (p) {
133
+ const cols = this.visibleCols();
134
+ this.columnReorderService.columns.set(cols);
135
+ this.columnReorderService.columnOrder.set(p.columnOrder);
136
+ this.columnReorderService.onColumnOrderChange.set(p.onColumnOrderChange);
137
+ this.columnReorderService.enabled.set(!!p.onColumnOrderChange);
138
+ }
139
+ });
140
+ // Wire virtual scroll service inputs
141
+ effect(() => {
142
+ const p = this.getProps();
143
+ if (p) {
144
+ this.virtualScrollService.totalRows.set(p.items.length);
145
+ }
146
+ });
147
+ }
148
+ // --- Helper methods ---
149
+ asColumnDef(colDef) {
150
+ return colDef;
151
+ }
152
+ visibleColIndex(col) {
153
+ return this.visibleCols().indexOf(col);
154
+ }
155
+ getColumnWidth(col) {
156
+ const overrides = this.columnSizingOverrides();
157
+ const override = overrides[col.columnId];
158
+ if (override)
159
+ return override.widthPx;
160
+ return col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
161
+ }
162
+ getFilterConfig(col) {
163
+ return getHeaderFilterConfig(col, this.headerFilterInput());
164
+ }
165
+ getCellDescriptor(item, col, rowIndex, colIdx) {
166
+ return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
167
+ }
168
+ resolveCellContent(col, item, displayValue) {
169
+ return resolveCellDisplayContent(col, item, displayValue);
170
+ }
171
+ resolveCellStyleFn(col, item) {
172
+ return resolveCellStyle(col, item);
173
+ }
174
+ getSelectValues(col) {
175
+ const params = col.cellEditorParams;
176
+ if (params && typeof params === 'object' && 'values' in params) {
177
+ return params.values.map(String);
178
+ }
179
+ return [];
180
+ }
181
+ formatDateForInput(value) {
182
+ if (!value)
183
+ return '';
184
+ const d = new Date(String(value));
185
+ if (Number.isNaN(d.getTime()))
186
+ return '';
187
+ return d.toISOString().split('T')[0];
188
+ }
189
+ // --- Event handlers ---
190
+ onWrapperMouseDown(event) {
191
+ this.lastMouseShift = event.shiftKey;
192
+ }
193
+ onGridKeyDown(event) {
194
+ this.state().interaction.handleGridKeyDown(event);
195
+ }
196
+ onCellMouseDown(event, rowIndex, globalColIndex) {
197
+ this.state().interaction.handleCellMouseDown(event, rowIndex, globalColIndex);
198
+ }
199
+ onCellClick(rowIndex, globalColIndex) {
200
+ this.state().interaction.setActiveCell({ rowIndex, columnIndex: globalColIndex });
201
+ }
202
+ onCellContextMenu(event) {
203
+ this.state().contextMenu.handleCellContextMenu(event);
204
+ }
205
+ onCellDblClick(rowId, columnId) {
206
+ this.state().editing.setEditingCell({ rowId, columnId });
207
+ }
208
+ onFillHandleMouseDown(event) {
209
+ this.state().interaction.handleFillHandleMouseDown(event);
210
+ }
211
+ onResizeStart(event, col) {
212
+ event.preventDefault();
213
+ const startX = event.clientX;
214
+ const startWidth = this.getColumnWidth(col);
215
+ const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
216
+ const onMove = (e) => {
217
+ const delta = e.clientX - startX;
218
+ const newWidth = Math.max(minWidth, startWidth + delta);
219
+ const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
220
+ this.state().layout.setColumnSizingOverrides(overrides);
221
+ this.columnSizingVersion.update(v => v + 1);
222
+ };
223
+ const onUp = () => {
224
+ window.removeEventListener('mousemove', onMove);
225
+ window.removeEventListener('mouseup', onUp);
226
+ const finalWidth = this.getColumnWidth(col);
227
+ this.state().layout.onColumnResized?.(col.columnId, finalWidth);
228
+ };
229
+ window.addEventListener('mousemove', onMove);
230
+ window.addEventListener('mouseup', onUp);
231
+ }
232
+ onSelectAllChange(event) {
233
+ const checked = event.target.checked;
234
+ this.state().rowSelection.handleSelectAll(!!checked);
235
+ }
236
+ onRowClick(event, rowId) {
237
+ const p = this.getProps();
238
+ if (p?.rowSelection !== 'single')
239
+ return;
240
+ const ids = this.selectedRowIds();
241
+ this.state().rowSelection.updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
242
+ }
243
+ onRowCheckboxChange(rowId, event, rowIndex) {
244
+ const checked = event.target.checked;
245
+ this.state().rowSelection.handleRowCheckboxChange(rowId, checked, rowIndex, this.lastMouseShift);
246
+ }
247
+ commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
248
+ this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
249
+ }
250
+ cancelEdit() {
251
+ this.state().editing.setEditingCell(null);
252
+ }
253
+ onEditorKeydown(event, item, columnId, oldValue, rowIndex, globalColIndex) {
254
+ if (event.key === 'Enter') {
255
+ event.preventDefault();
256
+ const newValue = event.target.value;
257
+ this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
258
+ }
259
+ else if (event.key === 'Escape') {
260
+ event.preventDefault();
261
+ this.cancelEdit();
262
+ }
263
+ }
264
+ closeContextMenu() {
265
+ this.state().contextMenu.closeContextMenu();
266
+ }
267
+ handleCopy() {
268
+ this.state().interaction.handleCopy();
269
+ }
270
+ handleCut() {
271
+ this.state().interaction.handleCut();
272
+ }
273
+ handlePaste() {
274
+ void this.state().interaction.handlePaste();
275
+ }
276
+ handleSelectAllCells() {
277
+ this.state().interaction.handleSelectAllCells();
278
+ }
279
+ onUndo() {
280
+ this.state().interaction.onUndo?.();
281
+ }
282
+ onRedo() {
283
+ this.state().interaction.onRedo?.();
284
+ }
285
+ onHeaderMouseDown(columnId, event) {
286
+ this.columnReorderService.handleHeaderMouseDown(columnId, event);
287
+ }
288
+ // --- Column pinning methods ---
289
+ onPinColumn(columnId, side) {
290
+ const props = this.getProps();
291
+ props?.onColumnPinned?.(columnId, side);
292
+ }
293
+ onUnpinColumn(columnId) {
294
+ const props = this.getProps();
295
+ props?.onColumnPinned?.(columnId, null);
296
+ }
297
+ isPinned(columnId) {
298
+ const props = this.getProps();
299
+ return props?.pinnedColumns?.[columnId];
300
+ }
301
+ getPinState(columnId) {
302
+ const pinned = this.isPinned(columnId);
303
+ return {
304
+ canPinLeft: pinned !== 'left',
305
+ canPinRight: pinned !== 'right',
306
+ canUnpin: !!pinned,
307
+ };
308
+ }
309
+ }
@@ -19,6 +19,12 @@ EmptyStateComponent = __decorate([
19
19
  selector: 'ogrid-empty-state',
20
20
  standalone: true,
21
21
  imports: [CommonModule],
22
+ styles: [`
23
+ .ogrid-empty-state-clear-btn {
24
+ background: none; border: none; color: inherit;
25
+ text-decoration: underline; cursor: pointer; padding: 0; font: inherit;
26
+ }
27
+ `],
22
28
  template: `
23
29
  @if (render()) {
24
30
  <ng-container [ngTemplateOutlet]="render()!"></ng-container>
@@ -26,7 +32,7 @@ EmptyStateComponent = __decorate([
26
32
  {{ message() }}
27
33
  } @else if (hasActiveFilters()) {
28
34
  No items match your current filters. Try adjusting your search or
29
- <button type="button" (click)="clearAll.emit()" style="background:none;border:none;color:inherit;text-decoration:underline;cursor:pointer;padding:0;font:inherit">
35
+ <button type="button" (click)="clearAll.emit()" class="ogrid-empty-state-clear-btn">
30
36
  clear all filters
31
37
  </button>
32
38
  to see all items.
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, input, output, effect, viewChild, DestroyRef, inject } from '@angular/core';
7
+ import { Component, input, output, viewChild, DestroyRef, inject } from '@angular/core';
8
8
  import { CommonModule } from '@angular/common';
9
9
  import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
10
10
  let GridContextMenuComponent = class GridContextMenuComponent {
@@ -35,11 +35,9 @@ let GridContextMenuComponent = class GridContextMenuComponent {
35
35
  if (e.key === 'Escape')
36
36
  this.close.emit();
37
37
  };
38
- effect(() => {
39
- // Re-register listeners when component renders
40
- document.addEventListener('mousedown', this.clickOutsideHandler, true);
41
- document.addEventListener('keydown', this.keyDownHandler, true);
42
- });
38
+ // Register listeners once on init (no signal dependencies needed)
39
+ document.addEventListener('mousedown', this.clickOutsideHandler, true);
40
+ document.addEventListener('keydown', this.keyDownHandler, true);
43
41
  this.destroyRef.onDestroy(() => {
44
42
  document.removeEventListener('mousedown', this.clickOutsideHandler, true);
45
43
  document.removeEventListener('keydown', this.keyDownHandler, true);
@@ -104,17 +104,19 @@ MarchingAntsOverlayComponent = __decorate([
104
104
  selector: 'ogrid-marching-ants-overlay',
105
105
  standalone: true,
106
106
  imports: [CommonModule],
107
+ styles: [`
108
+ .ogrid-marching-ants-svg { position: absolute; pointer-events: none; overflow: visible; }
109
+ .ogrid-marching-ants-svg--selection { z-index: 4; }
110
+ .ogrid-marching-ants-svg--clip { z-index: 5; }
111
+ `],
107
112
  template: `
108
113
  @if (selRect() && !clipRangeMatchesSel()) {
109
114
  <svg
110
- [style.position]="'absolute'"
115
+ class="ogrid-marching-ants-svg ogrid-marching-ants-svg--selection"
111
116
  [style.top.px]="selRect()!.top"
112
117
  [style.left.px]="selRect()!.left"
113
118
  [style.width.px]="selRect()!.width"
114
119
  [style.height.px]="selRect()!.height"
115
- [style.pointer-events]="'none'"
116
- [style.z-index]="4"
117
- [style.overflow]="'visible'"
118
120
  aria-hidden="true"
119
121
  >
120
122
  <rect
@@ -129,14 +131,11 @@ MarchingAntsOverlayComponent = __decorate([
129
131
  }
130
132
  @if (clipRect()) {
131
133
  <svg
132
- [style.position]="'absolute'"
134
+ class="ogrid-marching-ants-svg ogrid-marching-ants-svg--clip"
133
135
  [style.top.px]="clipRect()!.top"
134
136
  [style.left.px]="clipRect()!.left"
135
137
  [style.width.px]="clipRect()!.width"
136
138
  [style.height.px]="clipRect()!.height"
137
- [style.pointer-events]="'none'"
138
- [style.z-index]="5"
139
- [style.overflow]="'visible'"
140
139
  aria-hidden="true"
141
140
  >
142
141
  <rect
@@ -23,34 +23,47 @@ OGridLayoutComponent = __decorate([
23
23
  selector: 'ogrid-layout',
24
24
  standalone: true,
25
25
  imports: [CommonModule, SideBarComponent],
26
+ styles: [`
27
+ .ogrid-layout-root { display: flex; flex-direction: column; height: 100%; }
28
+ .ogrid-layout-container {
29
+ border: 1px solid var(--ogrid-border, #e0e0e0);
30
+ overflow: hidden; display: flex; flex-direction: column;
31
+ flex: 1; min-height: 0; background: var(--ogrid-bg, #fff);
32
+ }
33
+ .ogrid-layout-toolbar {
34
+ display: flex; justify-content: space-between; align-items: center;
35
+ padding: 6px 12px; background: var(--ogrid-header-bg, #f5f5f5);
36
+ gap: 8px; flex-wrap: wrap; min-height: 0;
37
+ }
38
+ .ogrid-layout-toolbar--has-below { border-bottom: none; }
39
+ .ogrid-layout-toolbar--no-below { border-bottom: 1px solid var(--ogrid-border, #e0e0e0); }
40
+ .ogrid-layout-toolbar-left { display: flex; align-items: center; gap: 8px; }
41
+ .ogrid-layout-toolbar-right { display: flex; align-items: center; gap: 8px; }
42
+ .ogrid-layout-toolbar-below {
43
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
44
+ padding: 6px 12px; background: var(--ogrid-header-bg, #f5f5f5);
45
+ }
46
+ .ogrid-layout-grid-area { width: 100%; min-width: 0; min-height: 0; flex: 1; display: flex; }
47
+ .ogrid-layout-grid-content { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
48
+ .ogrid-layout-footer {
49
+ border-top: 1px solid var(--ogrid-border, #e0e0e0);
50
+ background: var(--ogrid-header-bg, #f5f5f5); padding: 6px 12px;
51
+ }
52
+ `],
26
53
  template: `
27
- <div [class]="className() ?? ''" [style.display]="'flex'" [style.flex-direction]="'column'" [style.height]="'100%'">
28
- <div [style.border]="'1px solid var(--ogrid-border, #e0e0e0)'"
29
- [style.border-radius.px]="borderRadius"
30
- [style.overflow]="'hidden'"
31
- [style.display]="'flex'"
32
- [style.flex-direction]="'column'"
33
- [style.flex]="1"
34
- [style.min-height]="0"
35
- [style.background]="'var(--ogrid-bg, #fff)'"
36
- >
54
+ <div [class]="(className() ?? '') + ' ogrid-layout-root'">
55
+ <div class="ogrid-layout-container" [style.border-radius.px]="borderRadius">
37
56
  <!-- Toolbar strip -->
38
57
  @if (hasToolbar()) {
39
58
  <div
40
- [style.display]="'flex'"
41
- [style.justify-content]="'space-between'"
42
- [style.align-items]="'center'"
43
- [style.padding]="'6px 12px'"
44
- [style.background]="'var(--ogrid-header-bg, #f5f5f5)'"
45
- [style.gap.px]="8"
46
- [style.flex-wrap]="'wrap'"
47
- [style.min-height]="0"
48
- [style.border-bottom]="hasToolbarBelow() ? 'none' : '1px solid var(--ogrid-border, #e0e0e0)'"
59
+ class="ogrid-layout-toolbar"
60
+ [class.ogrid-layout-toolbar--has-below]="hasToolbarBelow()"
61
+ [class.ogrid-layout-toolbar--no-below]="!hasToolbarBelow()"
49
62
  >
50
- <div style="display:flex;align-items:center;gap:8px">
63
+ <div class="ogrid-layout-toolbar-left">
51
64
  <ng-content select="[toolbar]"></ng-content>
52
65
  </div>
53
- <div style="display:flex;align-items:center;gap:8px">
66
+ <div class="ogrid-layout-toolbar-right">
54
67
  <ng-content select="[toolbarEnd]"></ng-content>
55
68
  </div>
56
69
  </div>
@@ -58,18 +71,18 @@ OGridLayoutComponent = __decorate([
58
71
 
59
72
  <!-- Secondary toolbar row -->
60
73
  @if (hasToolbarBelow()) {
61
- <div style="border-bottom:1px solid var(--ogrid-border, #e0e0e0);padding:6px 12px;background:var(--ogrid-header-bg, #f5f5f5)">
74
+ <div class="ogrid-layout-toolbar-below">
62
75
  <ng-content select="[toolbarBelow]"></ng-content>
63
76
  </div>
64
77
  }
65
78
 
66
79
  <!-- Grid area -->
67
80
  @if (sideBar()) {
68
- <div style="width:100%;min-width:0;min-height:0;flex:1;display:flex">
81
+ <div class="ogrid-layout-grid-area">
69
82
  @if (sideBar()?.position === 'left') {
70
83
  <ogrid-sidebar [sideBarProps]="sideBar()"></ogrid-sidebar>
71
84
  }
72
- <div style="flex:1;min-width:0;min-height:0;display:flex;flex-direction:column">
85
+ <div class="ogrid-layout-grid-content">
73
86
  <ng-content></ng-content>
74
87
  </div>
75
88
  @if (sideBar()?.position !== 'left') {
@@ -77,14 +90,14 @@ OGridLayoutComponent = __decorate([
77
90
  }
78
91
  </div>
79
92
  } @else {
80
- <div style="width:100%;min-width:0;min-height:0;flex:1;display:flex;flex-direction:column">
93
+ <div class="ogrid-layout-grid-content">
81
94
  <ng-content></ng-content>
82
95
  </div>
83
96
  }
84
97
 
85
98
  <!-- Footer strip (pagination) -->
86
99
  @if (hasPagination()) {
87
- <div style="border-top:1px solid var(--ogrid-border, #e0e0e0);background:var(--ogrid-header-bg, #f5f5f5);padding:6px 12px">
100
+ <div class="ogrid-layout-footer">
88
101
  <ng-content select="[pagination]"></ng-content>
89
102
  </div>
90
103
  }
@@ -98,8 +98,70 @@ SideBarComponent = __decorate([
98
98
  selector: 'ogrid-sidebar',
99
99
  standalone: true,
100
100
  imports: [CommonModule],
101
+ styles: [`
102
+ .ogrid-sidebar-root { display: flex; flex-direction: row; flex-shrink: 0; }
103
+ .ogrid-sidebar-tab-strip {
104
+ display: flex; flex-direction: column;
105
+ width: var(--ogrid-sidebar-tab-size, 36px);
106
+ background: var(--ogrid-header-bg, #f5f5f5);
107
+ }
108
+ .ogrid-sidebar-tab-strip--left { border-right: 1px solid var(--ogrid-border, #e0e0e0); }
109
+ .ogrid-sidebar-tab-strip--right { border-left: 1px solid var(--ogrid-border, #e0e0e0); }
110
+ .ogrid-sidebar-tab {
111
+ width: var(--ogrid-sidebar-tab-size, 36px);
112
+ height: var(--ogrid-sidebar-tab-size, 36px);
113
+ border: none; cursor: pointer;
114
+ color: var(--ogrid-fg, #242424); font-size: 14px;
115
+ display: flex; align-items: center; justify-content: center;
116
+ background: transparent; font-weight: normal;
117
+ }
118
+ .ogrid-sidebar-tab--active { background: var(--ogrid-bg, #fff); font-weight: bold; }
119
+ .ogrid-sidebar-panel {
120
+ width: var(--ogrid-sidebar-panel-width, 240px);
121
+ display: flex; flex-direction: column; overflow: hidden;
122
+ background: var(--ogrid-bg, #fff); color: var(--ogrid-fg, #242424);
123
+ }
124
+ .ogrid-sidebar-panel--left { border-right: 1px solid var(--ogrid-border, #e0e0e0); }
125
+ .ogrid-sidebar-panel--right { border-left: 1px solid var(--ogrid-border, #e0e0e0); }
126
+ .ogrid-sidebar-panel-header {
127
+ display: flex; justify-content: space-between; align-items: center;
128
+ padding: 8px 12px; border-bottom: 1px solid var(--ogrid-border, #e0e0e0); font-weight: 600;
129
+ }
130
+ .ogrid-sidebar-panel-close {
131
+ border: none; background: transparent; cursor: pointer;
132
+ font-size: 16px; color: var(--ogrid-fg, #242424);
133
+ }
134
+ .ogrid-sidebar-panel-body { flex: 1; overflow-y: auto; padding: 8px 12px; }
135
+ .ogrid-sidebar-actions { display: flex; gap: 8px; margin-bottom: 8px; }
136
+ .ogrid-sidebar-action-btn {
137
+ flex: 1; cursor: pointer;
138
+ background: var(--ogrid-bg-subtle, #f3f2f1); color: var(--ogrid-fg, #242424);
139
+ border: 1px solid var(--ogrid-border, #e0e0e0); border-radius: 4px; padding: 4px 8px;
140
+ }
141
+ .ogrid-sidebar-col-label { display: flex; align-items: center; gap: 6px; padding: 2px 0; cursor: pointer; }
142
+ .ogrid-sidebar-empty { color: var(--ogrid-muted, #999); font-style: italic; }
143
+ .ogrid-sidebar-filter-group { margin-bottom: 12px; }
144
+ .ogrid-sidebar-filter-label { font-weight: 500; margin-bottom: 4px; font-size: 13px; }
145
+ .ogrid-sidebar-text-input {
146
+ width: 100%; box-sizing: border-box; padding: 4px 6px;
147
+ background: var(--ogrid-bg, #fff); color: var(--ogrid-fg, #242424);
148
+ border: 1px solid var(--ogrid-border, #e0e0e0); border-radius: 4px;
149
+ }
150
+ .ogrid-sidebar-date-row { display: flex; flex-direction: column; gap: 4px; }
151
+ .ogrid-sidebar-date-label { display: flex; align-items: center; gap: 4px; font-size: 12px; }
152
+ .ogrid-sidebar-date-input {
153
+ flex: 1; padding: 2px 4px;
154
+ background: var(--ogrid-bg, #fff); color: var(--ogrid-fg, #242424);
155
+ border: 1px solid var(--ogrid-border, #e0e0e0); border-radius: 4px;
156
+ }
157
+ .ogrid-sidebar-multiselect-list { max-height: 120px; overflow-y: auto; }
158
+ .ogrid-sidebar-multiselect-item {
159
+ display: flex; align-items: center; gap: 4px;
160
+ padding: 1px 0; cursor: pointer; font-size: 13px;
161
+ }
162
+ `],
101
163
  template: `
102
- <div style="display:flex;flex-direction:row;flex-shrink:0" role="complementary" aria-label="Side bar">
164
+ <div class="ogrid-sidebar-root" role="complementary" aria-label="Side bar">
103
165
  @if (sideBarProps()?.position === 'left') {
104
166
  <ng-container *ngTemplateOutlet="tabStripTpl"></ng-container>
105
167
  <ng-container *ngTemplateOutlet="panelContentTpl"></ng-container>
@@ -112,33 +174,21 @@ SideBarComponent = __decorate([
112
174
 
113
175
  <ng-template #tabStripTpl>
114
176
  <div
115
- [style.display]="'flex'"
116
- [style.flex-direction]="'column'"
117
- [style.width.px]="tabWidth"
118
- [style.background]="'var(--ogrid-header-bg, #f5f5f5)'"
119
- [style.border-left]="sideBarProps()?.position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : 'none'"
120
- [style.border-right]="sideBarProps()?.position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : 'none'"
177
+ class="ogrid-sidebar-tab-strip"
178
+ [class.ogrid-sidebar-tab-strip--left]="sideBarProps()?.position === 'left'"
179
+ [class.ogrid-sidebar-tab-strip--right]="sideBarProps()?.position === 'right'"
121
180
  role="tablist"
122
181
  aria-label="Side bar tabs"
123
182
  >
124
183
  @for (panel of sideBarProps()?.panels ?? []; track panel) {
125
184
  <button
126
185
  role="tab"
186
+ class="ogrid-sidebar-tab"
187
+ [class.ogrid-sidebar-tab--active]="sideBarProps()?.activePanel === panel"
127
188
  [attr.aria-selected]="sideBarProps()?.activePanel === panel"
128
189
  [attr.aria-label]="panelLabels[panel]"
129
190
  (click)="onTabClick(panel)"
130
191
  [title]="panelLabels[panel]"
131
- [style.width.px]="tabWidth"
132
- [style.height.px]="tabWidth"
133
- [style.border]="'none'"
134
- [style.cursor]="'pointer'"
135
- [style.color]="'var(--ogrid-fg, #242424)'"
136
- [style.font-size.px]="14"
137
- [style.display]="'flex'"
138
- [style.align-items]="'center'"
139
- [style.justify-content]="'center'"
140
- [style.background]="sideBarProps()?.activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent'"
141
- [style.font-weight]="sideBarProps()?.activePanel === panel ? 'bold' : 'normal'"
142
192
  >
143
193
  {{ panel === 'columns' ? '\u2261' : '\u2A65' }}
144
194
  </button>
@@ -150,28 +200,23 @@ SideBarComponent = __decorate([
150
200
  @if (sideBarProps()?.activePanel) {
151
201
  <div
152
202
  role="tabpanel"
203
+ class="ogrid-sidebar-panel"
204
+ [class.ogrid-sidebar-panel--left]="sideBarProps()?.position === 'left'"
205
+ [class.ogrid-sidebar-panel--right]="sideBarProps()?.position === 'right'"
153
206
  [attr.aria-label]="panelLabels[sideBarProps()!.activePanel!]"
154
- [style.width.px]="panelWidth"
155
- [style.display]="'flex'"
156
- [style.flex-direction]="'column'"
157
- [style.overflow]="'hidden'"
158
- [style.background]="'var(--ogrid-bg, #fff)'"
159
- [style.color]="'var(--ogrid-fg, #242424)'"
160
- [style.border-left]="sideBarProps()?.position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : 'none'"
161
- [style.border-right]="sideBarProps()?.position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : 'none'"
162
207
  >
163
- <div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid var(--ogrid-border, #e0e0e0);font-weight:600">
208
+ <div class="ogrid-sidebar-panel-header">
164
209
  <span>{{ panelLabels[sideBarProps()!.activePanel!] }}</span>
165
- <button (click)="sideBarProps()?.onPanelChange(null)" style="border:none;background:transparent;cursor:pointer;font-size:16px;color:var(--ogrid-fg, #242424)" aria-label="Close panel">&times;</button>
210
+ <button (click)="sideBarProps()?.onPanelChange(null)" class="ogrid-sidebar-panel-close" aria-label="Close panel">&times;</button>
166
211
  </div>
167
- <div style="flex:1;overflow-y:auto;padding:8px 12px">
212
+ <div class="ogrid-sidebar-panel-body">
168
213
  @if (sideBarProps()?.activePanel === 'columns') {
169
- <div style="display:flex;gap:8px;margin-bottom:8px">
170
- <button (click)="onSelectAll()" [disabled]="allVisible()" style="flex:1;cursor:pointer;background:var(--ogrid-bg-subtle, #f3f2f1);color:var(--ogrid-fg, #242424);border:1px solid var(--ogrid-border, #e0e0e0);border-radius:4px;padding:4px 8px">Select All</button>
171
- <button (click)="onClearAll()" style="flex:1;cursor:pointer;background:var(--ogrid-bg-subtle, #f3f2f1);color:var(--ogrid-fg, #242424);border:1px solid var(--ogrid-border, #e0e0e0);border-radius:4px;padding:4px 8px">Clear All</button>
214
+ <div class="ogrid-sidebar-actions">
215
+ <button (click)="onSelectAll()" [disabled]="allVisible()" class="ogrid-sidebar-action-btn">Select All</button>
216
+ <button (click)="onClearAll()" class="ogrid-sidebar-action-btn">Clear All</button>
172
217
  </div>
173
218
  @for (col of sideBarProps()?.columns ?? []; track col.columnId) {
174
- <label style="display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer">
219
+ <label class="ogrid-sidebar-col-label">
175
220
  <input type="checkbox" [checked]="sideBarProps()?.visibleColumns?.has(col.columnId)" (change)="onVisibilityChange(col.columnId, $any($event.target).checked)" [disabled]="col.required" />
176
221
  <span>{{ col.name }}</span>
177
222
  </label>
@@ -179,37 +224,37 @@ SideBarComponent = __decorate([
179
224
  }
180
225
  @if (sideBarProps()?.activePanel === 'filters') {
181
226
  @if ((sideBarProps()?.filterableColumns ?? []).length === 0) {
182
- <div style="color:var(--ogrid-muted, #999);font-style:italic">No filterable columns</div>
227
+ <div class="ogrid-sidebar-empty">No filterable columns</div>
183
228
  }
184
229
  @for (col of sideBarProps()?.filterableColumns ?? []; track col.columnId) {
185
- <div style="margin-bottom:12px">
186
- <div style="font-weight:500;margin-bottom:4px;font-size:13px">{{ col.name }}</div>
230
+ <div class="ogrid-sidebar-filter-group">
231
+ <div class="ogrid-sidebar-filter-label">{{ col.name }}</div>
187
232
  @if (col.filterType === 'text') {
188
233
  <input
189
234
  type="text"
235
+ class="ogrid-sidebar-text-input"
190
236
  [value]="getTextFilterValue(col.filterField)"
191
237
  (input)="onTextFilterChange(col.filterField, $any($event.target).value)"
192
238
  [placeholder]="'Filter ' + col.name + '...'"
193
239
  [attr.aria-label]="'Filter ' + col.name"
194
- style="width:100%;box-sizing:border-box;padding:4px 6px;background:var(--ogrid-bg, #fff);color:var(--ogrid-fg, #242424);border:1px solid var(--ogrid-border, #e0e0e0);border-radius:4px"
195
240
  />
196
241
  }
197
242
  @if (col.filterType === 'date') {
198
- <div style="display:flex;flex-direction:column;gap:4px">
199
- <label style="display:flex;align-items:center;gap:4px;font-size:12px">
243
+ <div class="ogrid-sidebar-date-row">
244
+ <label class="ogrid-sidebar-date-label">
200
245
  From:
201
- <input type="date" [value]="getDateFrom(col.filterField)" (change)="onDateFromChange(col.filterField, $any($event.target).value)" [attr.aria-label]="col.name + ' from date'" style="flex:1;padding:2px 4px;background:var(--ogrid-bg, #fff);color:var(--ogrid-fg, #242424);border:1px solid var(--ogrid-border, #e0e0e0);border-radius:4px" />
246
+ <input type="date" class="ogrid-sidebar-date-input" [value]="getDateFrom(col.filterField)" (change)="onDateFromChange(col.filterField, $any($event.target).value)" [attr.aria-label]="col.name + ' from date'" />
202
247
  </label>
203
- <label style="display:flex;align-items:center;gap:4px;font-size:12px">
248
+ <label class="ogrid-sidebar-date-label">
204
249
  To:
205
- <input type="date" [value]="getDateTo(col.filterField)" (change)="onDateToChange(col.filterField, $any($event.target).value)" [attr.aria-label]="col.name + ' to date'" style="flex:1;padding:2px 4px;background:var(--ogrid-bg, #fff);color:var(--ogrid-fg, #242424);border:1px solid var(--ogrid-border, #e0e0e0);border-radius:4px" />
250
+ <input type="date" class="ogrid-sidebar-date-input" [value]="getDateTo(col.filterField)" (change)="onDateToChange(col.filterField, $any($event.target).value)" [attr.aria-label]="col.name + ' to date'" />
206
251
  </label>
207
252
  </div>
208
253
  }
209
254
  @if (col.filterType === 'multiSelect') {
210
- <div style="max-height:120px;overflow-y:auto" role="group" [attr.aria-label]="col.name + ' options'">
255
+ <div class="ogrid-sidebar-multiselect-list" role="group" [attr.aria-label]="col.name + ' options'">
211
256
  @for (opt of getFilterOptions(col.filterField); track opt) {
212
- <label style="display:flex;align-items:center;gap:4px;padding:1px 0;cursor:pointer;font-size:13px">
257
+ <label class="ogrid-sidebar-multiselect-item">
213
258
  <input type="checkbox" [checked]="isMultiSelectChecked(col.filterField, opt)" (change)="onMultiSelectChange(col.filterField, opt, $any($event.target).checked)" />
214
259
  <span>{{ opt }}</span>
215
260
  </label>
package/dist/esm/index.js CHANGED
@@ -13,4 +13,5 @@ export { GridContextMenuComponent } from './components/grid-context-menu.compone
13
13
  export { SideBarComponent } from './components/sidebar.component';
14
14
  export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
15
15
  export { EmptyStateComponent } from './components/empty-state.component';
16
+ export { BaseDataGridTableComponent } from './components/base-datagrid-table.component';
16
17
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
@@ -108,9 +108,14 @@ let DataGridStateService = class DataGridStateService {
108
108
  const order = p.columnOrder;
109
109
  if (!order?.length)
110
110
  return filtered;
111
+ // Build index map for O(1) lookup instead of repeated O(n) indexOf
112
+ const orderMap = new Map();
113
+ for (let i = 0; i < order.length; i++) {
114
+ orderMap.set(order[i], i);
115
+ }
111
116
  return [...filtered].sort((a, b) => {
112
- const ia = order.indexOf(a.columnId);
113
- const ib = order.indexOf(b.columnId);
117
+ const ia = orderMap.get(a.columnId) ?? -1;
118
+ const ib = orderMap.get(b.columnId) ?? -1;
114
119
  if (ia === -1 && ib === -1)
115
120
  return 0;
116
121
  if (ia === -1)
@@ -241,14 +246,20 @@ let DataGridStateService = class DataGridStateService {
241
246
  this.resizeObserver.observe(el);
242
247
  measure();
243
248
  });
244
- // Cleanup on destroy
249
+ // Cleanup on destroy — null out refs to prevent accidental reuse after teardown
245
250
  this.destroyRef.onDestroy(() => {
246
- if (this.rafId)
251
+ if (this.rafId) {
247
252
  cancelAnimationFrame(this.rafId);
248
- if (this.autoScrollInterval)
253
+ this.rafId = 0;
254
+ }
255
+ if (this.autoScrollInterval) {
249
256
  clearInterval(this.autoScrollInterval);
250
- if (this.resizeObserver)
257
+ this.autoScrollInterval = null;
258
+ }
259
+ if (this.resizeObserver) {
251
260
  this.resizeObserver.disconnect();
261
+ this.resizeObserver = null;
262
+ }
252
263
  });
253
264
  // Clean up column sizing overrides for removed columns
254
265
  effect(() => {
@@ -0,0 +1,160 @@
1
+ import { ElementRef } from '@angular/core';
2
+ import { DataGridStateService } from '../services/datagrid-state.service';
3
+ import { ColumnReorderService } from '../services/column-reorder.service';
4
+ import { VirtualScrollService } from '../services/virtual-scroll.service';
5
+ import type { IOGridDataGridProps, IColumnDef, RowId } from '../types';
6
+ import type { HeaderFilterConfig, CellRenderDescriptor } from '../utils';
7
+ /**
8
+ * Abstract base class containing all shared TypeScript logic for DataGridTable components.
9
+ * Framework-specific UI packages extend this with their templates and style overrides.
10
+ *
11
+ * Subclasses must:
12
+ * 1. Provide a @Component decorator with template and styles
13
+ * 2. Call `initBase()` in the constructor (effects require injection context)
14
+ * 3. Implement abstract accessors for propsInput, wrapperRef, and tableContainerRef
15
+ */
16
+ export declare abstract class BaseDataGridTableComponent<T = any> {
17
+ readonly stateService: DataGridStateService<T>;
18
+ readonly columnReorderService: ColumnReorderService<T>;
19
+ readonly virtualScrollService: VirtualScrollService;
20
+ protected lastMouseShift: boolean;
21
+ readonly columnSizingVersion: import("@angular/core").WritableSignal<number>;
22
+ /** Return the IOGridDataGridProps from however the subclass receives them */
23
+ protected abstract getProps(): IOGridDataGridProps<T> | undefined;
24
+ /** Return the wrapper element ref */
25
+ protected abstract getWrapperRef(): ElementRef<HTMLElement> | undefined;
26
+ /** Return the table container element ref */
27
+ protected abstract getTableContainerRef(): ElementRef<HTMLElement> | undefined;
28
+ readonly state: import("@angular/core").Signal<import("../services/datagrid-state.service").DataGridStateResult<T>>;
29
+ readonly tableContainerEl: import("@angular/core").Signal<HTMLElement | null>;
30
+ readonly items: import("@angular/core").Signal<T[]>;
31
+ readonly getRowId: import("@angular/core").Signal<(item: T) => RowId>;
32
+ readonly isLoading: import("@angular/core").Signal<boolean>;
33
+ readonly loadingMessage: import("@angular/core").Signal<string>;
34
+ readonly freezeRows: import("@angular/core").Signal<number | undefined>;
35
+ readonly freezeCols: import("@angular/core").Signal<number | undefined>;
36
+ readonly layoutModeFit: import("@angular/core").Signal<boolean>;
37
+ readonly ariaLabel: import("@angular/core").Signal<string>;
38
+ readonly ariaLabelledBy: import("@angular/core").Signal<string | undefined>;
39
+ readonly emptyState: import("@angular/core").Signal<{
40
+ onClearAll: () => void;
41
+ hasActiveFilters: boolean;
42
+ message?: string;
43
+ render?: import("@angular/core").TemplateRef<unknown>;
44
+ } | undefined>;
45
+ readonly currentPage: import("@angular/core").Signal<number>;
46
+ readonly pageSize: import("@angular/core").Signal<number>;
47
+ readonly rowNumberOffset: import("@angular/core").Signal<number>;
48
+ readonly visibleCols: import("@angular/core").Signal<IColumnDef<T>[]>;
49
+ readonly hasCheckboxCol: import("@angular/core").Signal<boolean>;
50
+ readonly hasRowNumbersCol: import("@angular/core").Signal<boolean>;
51
+ readonly colOffset: import("@angular/core").Signal<number>;
52
+ readonly containerWidth: import("@angular/core").Signal<number>;
53
+ readonly minTableWidth: import("@angular/core").Signal<number>;
54
+ readonly desiredTableWidth: import("@angular/core").Signal<number>;
55
+ readonly columnSizingOverrides: import("@angular/core").Signal<Record<string, {
56
+ widthPx: number;
57
+ }>>;
58
+ readonly selectedRowIds: import("@angular/core").Signal<Set<RowId>>;
59
+ readonly allSelected: import("@angular/core").Signal<boolean>;
60
+ readonly someSelected: import("@angular/core").Signal<boolean>;
61
+ readonly editingCell: import("@angular/core").Signal<{
62
+ rowId: RowId;
63
+ columnId: string;
64
+ } | null>;
65
+ readonly pendingEditorValue: import("@angular/core").Signal<unknown>;
66
+ readonly activeCell: import("@angular/core").Signal<import("@alaarab/ogrid-core").IActiveCell | null>;
67
+ readonly selectionRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
68
+ readonly hasCellSelection: import("@angular/core").Signal<boolean>;
69
+ readonly cutRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
70
+ readonly copyRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").ISelectionRange | null>;
71
+ readonly canUndo: import("@angular/core").Signal<boolean>;
72
+ readonly canRedo: import("@angular/core").Signal<boolean>;
73
+ readonly isDragging: import("@angular/core").Signal<boolean>;
74
+ readonly menuPosition: import("@angular/core").Signal<{
75
+ x: number;
76
+ y: number;
77
+ } | null>;
78
+ readonly statusBarConfig: import("@angular/core").Signal<import("@alaarab/ogrid-core").IStatusBarProps | null>;
79
+ readonly showEmptyInGrid: import("@angular/core").Signal<boolean>;
80
+ readonly headerFilterInput: import("@angular/core").Signal<{
81
+ sortBy?: string;
82
+ sortDirection: "asc" | "desc";
83
+ onColumnSort: (columnKey: string) => void;
84
+ filters: import("@alaarab/ogrid-core").IFilters;
85
+ onFilterChange: (key: string, value: import("@alaarab/ogrid-core").FilterValue | undefined) => void;
86
+ filterOptions: Record<string, string[]>;
87
+ loadingFilterOptions: Record<string, boolean>;
88
+ peopleSearch?: (query: string) => Promise<import("@alaarab/ogrid-core").UserLike[]>;
89
+ }>;
90
+ readonly cellDescriptorInput: import("@angular/core").Signal<{
91
+ editingCell: {
92
+ rowId: RowId;
93
+ columnId: string;
94
+ } | null;
95
+ activeCell: import("@alaarab/ogrid-core").IActiveCell | null;
96
+ selectionRange: import("@alaarab/ogrid-core").ISelectionRange | null;
97
+ cutRange: import("@alaarab/ogrid-core").ISelectionRange | null;
98
+ copyRange: import("@alaarab/ogrid-core").ISelectionRange | null;
99
+ colOffset: number;
100
+ itemsLength: number;
101
+ getRowId: (item: T) => RowId;
102
+ editable?: boolean;
103
+ onCellValueChanged?: ((event: import("@alaarab/ogrid-core").ICellValueChangedEvent<T>) => void) | undefined;
104
+ isDragging: boolean;
105
+ }>;
106
+ readonly allowOverflowX: import("@angular/core").Signal<boolean>;
107
+ readonly selectionCellCount: import("@angular/core").Signal<number | undefined>;
108
+ readonly headerRows: import("@angular/core").Signal<import("@alaarab/ogrid-core").HeaderRow<T>[]>;
109
+ readonly columnLayouts: import("@angular/core").Signal<{
110
+ col: IColumnDef<T>;
111
+ pinnedLeft: boolean;
112
+ pinnedRight: boolean;
113
+ minWidth: number;
114
+ width: number;
115
+ }[]>;
116
+ /**
117
+ * Initialize base wiring effects. Must be called from subclass constructor
118
+ * (effects need to run inside an injection context).
119
+ */
120
+ protected initBase(): void;
121
+ asColumnDef(colDef: unknown): IColumnDef<T>;
122
+ visibleColIndex(col: IColumnDef<T>): number;
123
+ getColumnWidth(col: IColumnDef<T>): number;
124
+ getFilterConfig(col: IColumnDef<T>): HeaderFilterConfig;
125
+ getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
126
+ resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): string;
127
+ resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
128
+ getSelectValues(col: IColumnDef<T>): string[];
129
+ formatDateForInput(value: unknown): string;
130
+ onWrapperMouseDown(event: MouseEvent): void;
131
+ onGridKeyDown(event: KeyboardEvent): void;
132
+ onCellMouseDown(event: MouseEvent, rowIndex: number, globalColIndex: number): void;
133
+ onCellClick(rowIndex: number, globalColIndex: number): void;
134
+ onCellContextMenu(event: MouseEvent): void;
135
+ onCellDblClick(rowId: RowId, columnId: string): void;
136
+ onFillHandleMouseDown(event: MouseEvent): void;
137
+ onResizeStart(event: MouseEvent, col: IColumnDef<T>): void;
138
+ onSelectAllChange(event: Event): void;
139
+ onRowClick(event: MouseEvent, rowId: RowId): void;
140
+ onRowCheckboxChange(rowId: RowId, event: Event, rowIndex: number): void;
141
+ commitEdit(item: T, columnId: string, oldValue: unknown, newValue: unknown, rowIndex: number, globalColIndex: number): void;
142
+ cancelEdit(): void;
143
+ onEditorKeydown(event: KeyboardEvent, item: T, columnId: string, oldValue: unknown, rowIndex: number, globalColIndex: number): void;
144
+ closeContextMenu(): void;
145
+ handleCopy(): void;
146
+ handleCut(): void;
147
+ handlePaste(): void;
148
+ handleSelectAllCells(): void;
149
+ onUndo(): void;
150
+ onRedo(): void;
151
+ onHeaderMouseDown(columnId: string, event: MouseEvent): void;
152
+ onPinColumn(columnId: string, side: 'left' | 'right'): void;
153
+ onUnpinColumn(columnId: string): void;
154
+ isPinned(columnId: string): 'left' | 'right' | undefined;
155
+ getPinState(columnId: string): {
156
+ canPinLeft: boolean;
157
+ canPinRight: boolean;
158
+ canUnpin: boolean;
159
+ };
160
+ }
@@ -17,5 +17,6 @@ export { SideBarComponent } from './components/sidebar.component';
17
17
  export type { SideBarProps, SideBarFilterColumn } from './components/sidebar.component';
18
18
  export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
19
19
  export { EmptyStateComponent } from './components/empty-state.component';
20
+ export { BaseDataGridTableComponent } from './components/base-datagrid-table.component';
20
21
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
21
22
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -22,7 +22,7 @@
22
22
  "files": ["dist", "README.md", "LICENSE"],
23
23
  "engines": { "node": ">=18" },
24
24
  "dependencies": {
25
- "@alaarab/ogrid-core": "2.0.4"
25
+ "@alaarab/ogrid-core": "2.0.6"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@angular/core": "^21.0.0",
@@ -34,9 +34,10 @@
34
34
  "@angular/compiler": "^21.1.4",
35
35
  "@angular/platform-browser": "^21.1.4",
36
36
  "@angular/platform-browser-dynamic": "^21.1.4",
37
- "rxjs": "^7.8.0",
37
+ "rxjs": "^7.8.2",
38
38
  "zone.js": "^0.15.0",
39
- "typescript": "^5.7.3"
39
+ "typescript": "^5.9.3"
40
40
  },
41
+ "sideEffects": false,
41
42
  "publishConfig": { "access": "public" }
42
43
  }