@alaarab/ogrid-angular 2.0.5 → 2.0.7

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,308 @@
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
+ // Pinning state
67
+ this.pinnedColumnsMap = computed(() => this.state().pinning.pinnedColumns);
68
+ this.allowOverflowX = computed(() => {
69
+ const p = this.getProps();
70
+ if (p?.suppressHorizontalScroll)
71
+ return false;
72
+ const cw = this.containerWidth();
73
+ const mtw = this.minTableWidth();
74
+ const dtw = this.desiredTableWidth();
75
+ return cw > 0 && (mtw > cw || dtw > cw);
76
+ });
77
+ this.selectionCellCount = computed(() => {
78
+ const sr = this.selectionRange();
79
+ if (!sr)
80
+ return undefined;
81
+ return (Math.abs(sr.endRow - sr.startRow) + 1) * (Math.abs(sr.endCol - sr.startCol) + 1);
82
+ });
83
+ // Header rows from column definition
84
+ this.headerRows = computed(() => {
85
+ const p = this.getProps();
86
+ if (!p)
87
+ return [];
88
+ return buildHeaderRows(p.columns, p.visibleColumns);
89
+ });
90
+ // Pre-computed column layouts
91
+ this.columnLayouts = computed(() => {
92
+ const cols = this.visibleCols();
93
+ const fc = this.freezeCols();
94
+ const props = this.getProps();
95
+ const pinnedCols = props?.pinnedColumns ?? {};
96
+ return cols.map((col, colIdx) => {
97
+ const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
98
+ const runtimePinned = pinnedCols[col.columnId];
99
+ const pinnedLeft = runtimePinned === 'left' || col.pinned === 'left' || (isFreezeCol && colIdx === 0);
100
+ const pinnedRight = runtimePinned === 'right' || col.pinned === 'right';
101
+ const w = this.getColumnWidth(col);
102
+ return {
103
+ col,
104
+ pinnedLeft,
105
+ pinnedRight,
106
+ minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
107
+ width: w,
108
+ };
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Initialize base wiring effects. Must be called from subclass constructor
114
+ * (effects need to run inside an injection context).
115
+ */
116
+ initBase() {
117
+ // Wire props to state service
118
+ effect(() => {
119
+ const p = this.getProps();
120
+ if (p)
121
+ this.stateService.props.set(p);
122
+ });
123
+ // Wire wrapper element
124
+ effect(() => {
125
+ const el = this.getWrapperRef()?.nativeElement;
126
+ if (el) {
127
+ this.stateService.wrapperEl.set(el);
128
+ this.columnReorderService.wrapperEl.set(el);
129
+ }
130
+ });
131
+ // Wire column reorder service inputs
132
+ effect(() => {
133
+ const p = this.getProps();
134
+ if (p) {
135
+ const cols = this.visibleCols();
136
+ this.columnReorderService.columns.set(cols);
137
+ this.columnReorderService.columnOrder.set(p.columnOrder);
138
+ this.columnReorderService.onColumnOrderChange.set(p.onColumnOrderChange);
139
+ this.columnReorderService.enabled.set(!!p.onColumnOrderChange);
140
+ }
141
+ });
142
+ // Wire virtual scroll service inputs
143
+ effect(() => {
144
+ const p = this.getProps();
145
+ if (p) {
146
+ this.virtualScrollService.totalRows.set(p.items.length);
147
+ }
148
+ });
149
+ }
150
+ // --- Helper methods ---
151
+ asColumnDef(colDef) {
152
+ return colDef;
153
+ }
154
+ visibleColIndex(col) {
155
+ return this.visibleCols().indexOf(col);
156
+ }
157
+ getColumnWidth(col) {
158
+ const overrides = this.columnSizingOverrides();
159
+ const override = overrides[col.columnId];
160
+ if (override)
161
+ return override.widthPx;
162
+ return col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
163
+ }
164
+ getFilterConfig(col) {
165
+ return getHeaderFilterConfig(col, this.headerFilterInput());
166
+ }
167
+ getCellDescriptor(item, col, rowIndex, colIdx) {
168
+ return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
169
+ }
170
+ resolveCellContent(col, item, displayValue) {
171
+ return resolveCellDisplayContent(col, item, displayValue);
172
+ }
173
+ resolveCellStyleFn(col, item) {
174
+ return resolveCellStyle(col, item);
175
+ }
176
+ getSelectValues(col) {
177
+ const params = col.cellEditorParams;
178
+ if (params && typeof params === 'object' && 'values' in params) {
179
+ return params.values.map(String);
180
+ }
181
+ return [];
182
+ }
183
+ formatDateForInput(value) {
184
+ if (!value)
185
+ return '';
186
+ const d = new Date(String(value));
187
+ if (Number.isNaN(d.getTime()))
188
+ return '';
189
+ return d.toISOString().split('T')[0];
190
+ }
191
+ // --- Event handlers ---
192
+ onWrapperMouseDown(event) {
193
+ this.lastMouseShift = event.shiftKey;
194
+ }
195
+ onGridKeyDown(event) {
196
+ this.state().interaction.handleGridKeyDown(event);
197
+ }
198
+ onCellMouseDown(event, rowIndex, globalColIndex) {
199
+ this.state().interaction.handleCellMouseDown(event, rowIndex, globalColIndex);
200
+ }
201
+ onCellClick(rowIndex, globalColIndex) {
202
+ this.state().interaction.setActiveCell({ rowIndex, columnIndex: globalColIndex });
203
+ }
204
+ onCellContextMenu(event) {
205
+ this.state().contextMenu.handleCellContextMenu(event);
206
+ }
207
+ onCellDblClick(rowId, columnId) {
208
+ this.state().editing.setEditingCell({ rowId, columnId });
209
+ }
210
+ onFillHandleMouseDown(event) {
211
+ this.state().interaction.handleFillHandleMouseDown(event);
212
+ }
213
+ onResizeStart(event, col) {
214
+ event.preventDefault();
215
+ const startX = event.clientX;
216
+ const startWidth = this.getColumnWidth(col);
217
+ const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
218
+ const onMove = (e) => {
219
+ const delta = e.clientX - startX;
220
+ const newWidth = Math.max(minWidth, startWidth + delta);
221
+ const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
222
+ this.state().layout.setColumnSizingOverrides(overrides);
223
+ this.columnSizingVersion.update(v => v + 1);
224
+ };
225
+ const onUp = () => {
226
+ window.removeEventListener('mousemove', onMove);
227
+ window.removeEventListener('mouseup', onUp);
228
+ const finalWidth = this.getColumnWidth(col);
229
+ this.state().layout.onColumnResized?.(col.columnId, finalWidth);
230
+ };
231
+ window.addEventListener('mousemove', onMove);
232
+ window.addEventListener('mouseup', onUp);
233
+ }
234
+ onSelectAllChange(event) {
235
+ const checked = event.target.checked;
236
+ this.state().rowSelection.handleSelectAll(!!checked);
237
+ }
238
+ onRowClick(event, rowId) {
239
+ const p = this.getProps();
240
+ if (p?.rowSelection !== 'single')
241
+ return;
242
+ const ids = this.selectedRowIds();
243
+ this.state().rowSelection.updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
244
+ }
245
+ onRowCheckboxChange(rowId, event, rowIndex) {
246
+ const checked = event.target.checked;
247
+ this.state().rowSelection.handleRowCheckboxChange(rowId, checked, rowIndex, this.lastMouseShift);
248
+ }
249
+ commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
250
+ this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
251
+ }
252
+ cancelEdit() {
253
+ this.state().editing.setEditingCell(null);
254
+ }
255
+ onEditorKeydown(event, item, columnId, oldValue, rowIndex, globalColIndex) {
256
+ if (event.key === 'Enter') {
257
+ event.preventDefault();
258
+ const newValue = event.target.value;
259
+ this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
260
+ }
261
+ else if (event.key === 'Escape') {
262
+ event.preventDefault();
263
+ this.cancelEdit();
264
+ }
265
+ }
266
+ closeContextMenu() {
267
+ this.state().contextMenu.closeContextMenu();
268
+ }
269
+ handleCopy() {
270
+ this.state().interaction.handleCopy();
271
+ }
272
+ handleCut() {
273
+ this.state().interaction.handleCut();
274
+ }
275
+ handlePaste() {
276
+ void this.state().interaction.handlePaste();
277
+ }
278
+ handleSelectAllCells() {
279
+ this.state().interaction.handleSelectAllCells();
280
+ }
281
+ onUndo() {
282
+ this.state().interaction.onUndo?.();
283
+ }
284
+ onRedo() {
285
+ this.state().interaction.onRedo?.();
286
+ }
287
+ onHeaderMouseDown(columnId, event) {
288
+ this.columnReorderService.handleHeaderMouseDown(columnId, event);
289
+ }
290
+ // --- Column pinning methods ---
291
+ onPinColumn(columnId, side) {
292
+ this.state().pinning.pinColumn(columnId, side);
293
+ }
294
+ onUnpinColumn(columnId) {
295
+ this.state().pinning.unpinColumn(columnId);
296
+ }
297
+ isPinned(columnId) {
298
+ return this.state().pinning.isPinned(columnId);
299
+ }
300
+ getPinState(columnId) {
301
+ const pinned = this.isPinned(columnId);
302
+ return {
303
+ canPinLeft: pinned !== 'left',
304
+ canPinRight: pinned !== 'right',
305
+ canUnpin: !!pinned,
306
+ };
307
+ }
308
+ }
@@ -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.
@@ -52,7 +52,7 @@ let MarchingAntsOverlayComponent = class MarchingAntsOverlayComponent {
52
52
  const selRange = this.selectionRange();
53
53
  const clipRange = this.copyRange() ?? this.cutRange();
54
54
  const colOff = this.colOffset();
55
- const _version = this.columnSizingVersion(); // Track column resize changes
55
+ void this.columnSizingVersion(); // Track column resize changes
56
56
  if (this.resizeObserver) {
57
57
  this.resizeObserver.disconnect();
58
58
  this.resizeObserver = null;
@@ -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';
@@ -53,6 +53,10 @@ let DataGridStateService = class DataGridStateService {
53
53
  this.autoScrollInterval = null;
54
54
  // ResizeObserver
55
55
  this.resizeObserver = null;
56
+ // Header menu state (for column pinning UI)
57
+ this.headerMenuIsOpenSig = signal(false);
58
+ this.headerMenuOpenForColumnSig = signal(null);
59
+ this.headerMenuAnchorElementSig = signal(null);
56
60
  // --- Derived computed ---
57
61
  this.propsResolved = computed(() => this.props());
58
62
  this.cellSelection = computed(() => {
@@ -949,6 +953,59 @@ let DataGridStateService = class DataGridStateService {
949
953
  this.fillDragStart = { startRow: range.startRow, startCol: range.startCol };
950
954
  this.setupFillHandleDrag();
951
955
  }
956
+ // --- Column pinning ---
957
+ pinColumn(columnId, side) {
958
+ const props = this.props();
959
+ props?.onColumnPinned?.(columnId, side);
960
+ }
961
+ unpinColumn(columnId) {
962
+ const props = this.props();
963
+ props?.onColumnPinned?.(columnId, null);
964
+ }
965
+ isPinned(columnId) {
966
+ const props = this.props();
967
+ return props?.pinnedColumns?.[columnId];
968
+ }
969
+ getPinState(columnId) {
970
+ const pinned = this.isPinned(columnId);
971
+ return {
972
+ canPinLeft: pinned !== 'left',
973
+ canPinRight: pinned !== 'right',
974
+ canUnpin: !!pinned,
975
+ };
976
+ }
977
+ // --- Header menu ---
978
+ openHeaderMenu(columnId, anchorEl) {
979
+ this.headerMenuOpenForColumnSig.set(columnId);
980
+ this.headerMenuAnchorElementSig.set(anchorEl);
981
+ this.headerMenuIsOpenSig.set(true);
982
+ }
983
+ closeHeaderMenu() {
984
+ this.headerMenuIsOpenSig.set(false);
985
+ this.headerMenuOpenForColumnSig.set(null);
986
+ this.headerMenuAnchorElementSig.set(null);
987
+ }
988
+ headerMenuPinLeft() {
989
+ const col = this.headerMenuOpenForColumnSig();
990
+ if (col && this.isPinned(col) !== 'left') {
991
+ this.pinColumn(col, 'left');
992
+ this.closeHeaderMenu();
993
+ }
994
+ }
995
+ headerMenuPinRight() {
996
+ const col = this.headerMenuOpenForColumnSig();
997
+ if (col && this.isPinned(col) !== 'right') {
998
+ this.pinColumn(col, 'right');
999
+ this.closeHeaderMenu();
1000
+ }
1001
+ }
1002
+ headerMenuUnpin() {
1003
+ const col = this.headerMenuOpenForColumnSig();
1004
+ if (col && this.isPinned(col)) {
1005
+ this.unpinColumn(col);
1006
+ this.closeHeaderMenu();
1007
+ }
1008
+ }
952
1009
  // --- Get state result ---
953
1010
  getState() {
954
1011
  const p = this.props();
@@ -1048,7 +1105,29 @@ let DataGridStateService = class DataGridStateService {
1048
1105
  showEmptyInGrid: this.showEmptyInGrid(),
1049
1106
  onCellError: p?.onCellError,
1050
1107
  };
1051
- return { layout, rowSelection, editing, interaction, contextMenu, viewModels };
1108
+ // --- Pinning ---
1109
+ const openForColumn = this.headerMenuOpenForColumnSig();
1110
+ const currentPinState = openForColumn ? (p?.pinnedColumns?.[openForColumn]) : undefined;
1111
+ const pinning = {
1112
+ pinnedColumns: p?.pinnedColumns ?? {},
1113
+ pinColumn: (columnId, side) => this.pinColumn(columnId, side),
1114
+ unpinColumn: (columnId) => this.unpinColumn(columnId),
1115
+ isPinned: (columnId) => this.isPinned(columnId),
1116
+ headerMenu: {
1117
+ isOpen: this.headerMenuIsOpenSig(),
1118
+ openForColumn,
1119
+ anchorElement: this.headerMenuAnchorElementSig(),
1120
+ open: (columnId, anchorEl) => this.openHeaderMenu(columnId, anchorEl),
1121
+ close: () => this.closeHeaderMenu(),
1122
+ handlePinLeft: () => this.headerMenuPinLeft(),
1123
+ handlePinRight: () => this.headerMenuPinRight(),
1124
+ handleUnpin: () => this.headerMenuUnpin(),
1125
+ canPinLeft: currentPinState !== 'left',
1126
+ canPinRight: currentPinState !== 'right',
1127
+ canUnpin: !!currentPinState,
1128
+ },
1129
+ };
1130
+ return { layout, rowSelection, editing, interaction, contextMenu, viewModels, pinning };
1052
1131
  }
1053
1132
  // --- Private helpers ---
1054
1133
  getEffectiveRange() {
@@ -52,6 +52,7 @@ let OGridService = class OGridService {
52
52
  this.suppressHorizontalScroll = signal(undefined);
53
53
  this.editable = signal(undefined);
54
54
  this.cellSelection = signal(undefined);
55
+ this.density = signal('normal');
55
56
  this.onCellValueChanged = signal(undefined);
56
57
  this.onUndo = signal(undefined);
57
58
  this.onRedo = signal(undefined);
@@ -225,6 +226,7 @@ let OGridService = class OGridService {
225
226
  freezeCols: this.freezeCols(),
226
227
  editable: this.editable(),
227
228
  cellSelection: this.cellSelection(),
229
+ density: this.density(),
228
230
  onCellValueChanged: this.onCellValueChanged(),
229
231
  onUndo: this.onUndo(),
230
232
  onRedo: this.onRedo(),
@@ -428,7 +430,7 @@ let OGridService = class OGridService {
428
430
  handleColumnPinned(columnId, pinned) {
429
431
  this.pinnedOverrides.update((prev) => {
430
432
  if (pinned === null) {
431
- const { [columnId]: _, ...rest } = prev;
433
+ const { [columnId]: _removed, ...rest } = prev;
432
434
  return rest;
433
435
  }
434
436
  return { ...prev, [columnId]: pinned };
@@ -487,6 +489,8 @@ let OGridService = class OGridService {
487
489
  this.editable.set(props.editable);
488
490
  if (props.cellSelection !== undefined)
489
491
  this.cellSelection.set(props.cellSelection);
492
+ if (props.density !== undefined)
493
+ this.density.set(props.density);
490
494
  if (props.onCellValueChanged)
491
495
  this.onCellValueChanged.set(props.onCellValueChanged);
492
496
  if (props.onUndo)
@@ -0,0 +1,161 @@
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 = unknown> {
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 pinnedColumnsMap: import("@angular/core").Signal<Record<string, "left" | "right">>;
107
+ readonly allowOverflowX: import("@angular/core").Signal<boolean>;
108
+ readonly selectionCellCount: import("@angular/core").Signal<number | undefined>;
109
+ readonly headerRows: import("@angular/core").Signal<import("@alaarab/ogrid-core").HeaderRow<T>[]>;
110
+ readonly columnLayouts: import("@angular/core").Signal<{
111
+ col: IColumnDef<T>;
112
+ pinnedLeft: boolean;
113
+ pinnedRight: boolean;
114
+ minWidth: number;
115
+ width: number;
116
+ }[]>;
117
+ /**
118
+ * Initialize base wiring effects. Must be called from subclass constructor
119
+ * (effects need to run inside an injection context).
120
+ */
121
+ protected initBase(): void;
122
+ asColumnDef(colDef: unknown): IColumnDef<T>;
123
+ visibleColIndex(col: IColumnDef<T>): number;
124
+ getColumnWidth(col: IColumnDef<T>): number;
125
+ getFilterConfig(col: IColumnDef<T>): HeaderFilterConfig;
126
+ getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
127
+ resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): string;
128
+ resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
129
+ getSelectValues(col: IColumnDef<T>): string[];
130
+ formatDateForInput(value: unknown): string;
131
+ onWrapperMouseDown(event: MouseEvent): void;
132
+ onGridKeyDown(event: KeyboardEvent): void;
133
+ onCellMouseDown(event: MouseEvent, rowIndex: number, globalColIndex: number): void;
134
+ onCellClick(rowIndex: number, globalColIndex: number): void;
135
+ onCellContextMenu(event: MouseEvent): void;
136
+ onCellDblClick(rowId: RowId, columnId: string): void;
137
+ onFillHandleMouseDown(event: MouseEvent): void;
138
+ onResizeStart(event: MouseEvent, col: IColumnDef<T>): void;
139
+ onSelectAllChange(event: Event): void;
140
+ onRowClick(event: MouseEvent, rowId: RowId): void;
141
+ onRowCheckboxChange(rowId: RowId, event: Event, rowIndex: number): void;
142
+ commitEdit(item: T, columnId: string, oldValue: unknown, newValue: unknown, rowIndex: number, globalColIndex: number): void;
143
+ cancelEdit(): void;
144
+ onEditorKeydown(event: KeyboardEvent, item: T, columnId: string, oldValue: unknown, rowIndex: number, globalColIndex: number): void;
145
+ closeContextMenu(): void;
146
+ handleCopy(): void;
147
+ handleCut(): void;
148
+ handlePaste(): void;
149
+ handleSelectAllCells(): void;
150
+ onUndo(): void;
151
+ onRedo(): void;
152
+ onHeaderMouseDown(columnId: string, event: MouseEvent): void;
153
+ onPinColumn(columnId: string, side: 'left' | 'right'): void;
154
+ onUnpinColumn(columnId: string): void;
155
+ isPinned(columnId: string): 'left' | 'right' | undefined;
156
+ getPinState(columnId: string): {
157
+ canPinLeft: boolean;
158
+ canPinRight: boolean;
159
+ canUnpin: boolean;
160
+ };
161
+ }
@@ -6,7 +6,7 @@ export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types
6
6
  export { OGridService } from './services/ogrid.service';
7
7
  export type { ColumnChooserPlacement, OGridPagination, OGridColumnChooser, OGridFilters, OGridSideBarState, } from './services/ogrid.service';
8
8
  export { DataGridStateService } from './services/datagrid-state.service';
9
- export type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridStateResult, } from './services/datagrid-state.service';
9
+ export type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, DataGridStateResult, } from './services/datagrid-state.service';
10
10
  export { ColumnReorderService } from './services/column-reorder.service';
11
11
  export { VirtualScrollService } from './services/virtual-scroll.service';
12
12
  export type { IVirtualScrollConfig } from './services/virtual-scroll.service';
@@ -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';
@@ -115,6 +115,26 @@ export interface DataGridViewModelState<T> {
115
115
  showEmptyInGrid: boolean;
116
116
  onCellError?: (error: Error) => void;
117
117
  }
118
+ /** Column pinning state and column header menu. */
119
+ export interface DataGridPinningState {
120
+ pinnedColumns: Record<string, 'left' | 'right'>;
121
+ pinColumn: (columnId: string, side: 'left' | 'right') => void;
122
+ unpinColumn: (columnId: string) => void;
123
+ isPinned: (columnId: string) => 'left' | 'right' | undefined;
124
+ headerMenu: {
125
+ isOpen: boolean;
126
+ openForColumn: string | null;
127
+ anchorElement: HTMLElement | null;
128
+ open: (columnId: string, anchorEl: HTMLElement) => void;
129
+ close: () => void;
130
+ handlePinLeft: () => void;
131
+ handlePinRight: () => void;
132
+ handleUnpin: () => void;
133
+ canPinLeft: boolean;
134
+ canPinRight: boolean;
135
+ canUnpin: boolean;
136
+ };
137
+ }
118
138
  export interface DataGridStateResult<T> {
119
139
  layout: DataGridLayoutState<T>;
120
140
  rowSelection: DataGridRowSelectionState;
@@ -122,6 +142,7 @@ export interface DataGridStateResult<T> {
122
142
  interaction: DataGridCellInteractionState;
123
143
  contextMenu: DataGridContextMenuState;
124
144
  viewModels: DataGridViewModelState<T>;
145
+ pinning: DataGridPinningState;
125
146
  }
126
147
  /**
127
148
  * Single orchestration service for DataGridTable. Takes grid props,
@@ -161,6 +182,9 @@ export declare class DataGridStateService<T> {
161
182
  private lastMousePos;
162
183
  private autoScrollInterval;
163
184
  private resizeObserver;
185
+ private readonly headerMenuIsOpenSig;
186
+ private readonly headerMenuOpenForColumnSig;
187
+ private readonly headerMenuAnchorElementSig;
164
188
  private readonly propsResolved;
165
189
  readonly cellSelection: import("@angular/core").Signal<boolean>;
166
190
  private readonly wrappedOnCellValueChanged;
@@ -227,6 +251,19 @@ export declare class DataGridStateService<T> {
227
251
  redo(): void;
228
252
  handleGridKeyDown(e: KeyboardEvent): void;
229
253
  handleFillHandleMouseDown(e: MouseEvent): void;
254
+ pinColumn(columnId: string, side: 'left' | 'right'): void;
255
+ unpinColumn(columnId: string): void;
256
+ isPinned(columnId: string): 'left' | 'right' | undefined;
257
+ getPinState(columnId: string): {
258
+ canPinLeft: boolean;
259
+ canPinRight: boolean;
260
+ canUnpin: boolean;
261
+ };
262
+ openHeaderMenu(columnId: string, anchorEl: HTMLElement): void;
263
+ closeHeaderMenu(): void;
264
+ headerMenuPinLeft(): void;
265
+ headerMenuPinRight(): void;
266
+ headerMenuUnpin(): void;
230
267
  getState(): DataGridStateResult<T>;
231
268
  private getEffectiveRange;
232
269
  private onWindowMouseMove;
@@ -86,6 +86,7 @@ export declare class OGridService<T> {
86
86
  readonly suppressHorizontalScroll: import("@angular/core").WritableSignal<boolean | undefined>;
87
87
  readonly editable: import("@angular/core").WritableSignal<boolean | undefined>;
88
88
  readonly cellSelection: import("@angular/core").WritableSignal<boolean | undefined>;
89
+ readonly density: import("@angular/core").WritableSignal<"compact" | "normal" | "comfortable">;
89
90
  readonly onCellValueChanged: import("@angular/core").WritableSignal<((event: ICellValueChangedEvent<T>) => void) | undefined>;
90
91
  readonly onUndo: import("@angular/core").WritableSignal<(() => void) | undefined>;
91
92
  readonly onRedo: import("@angular/core").WritableSignal<(() => void) | undefined>;
@@ -32,6 +32,7 @@ interface IOGridBaseProps<T> {
32
32
  freezeCols?: number;
33
33
  editable?: boolean;
34
34
  cellSelection?: boolean;
35
+ density?: 'compact' | 'normal' | 'comfortable';
35
36
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
36
37
  onUndo?: () => void;
37
38
  onRedo?: () => void;
@@ -99,6 +100,7 @@ export interface IOGridDataGridProps<T> {
99
100
  loadingMessage?: string;
100
101
  editable?: boolean;
101
102
  cellSelection?: boolean;
103
+ density?: 'compact' | 'normal' | 'comfortable';
102
104
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
103
105
  onUndo?: () => void;
104
106
  onRedo?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
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.5"
25
+ "@alaarab/ogrid-core": "2.0.7"
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
  }