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