@gp-grid/angular 0.10.3
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/LICENSE +203 -0
- package/README.md +78 -0
- package/ng-package.json +10 -0
- package/package.json +71 -0
- package/src/lib/components/filter-popup/filter-logic.ts +243 -0
- package/src/lib/components/filter-popup.component.ts +236 -0
- package/src/lib/components/filter-popup.template.ts +182 -0
- package/src/lib/components/grid-body.component.ts +284 -0
- package/src/lib/components/grid-body.template.ts +93 -0
- package/src/lib/components/grid-header.component.ts +206 -0
- package/src/lib/components/grid-overlays.component.ts +140 -0
- package/src/lib/components/index.ts +4 -0
- package/src/lib/createGridData.ts +82 -0
- package/src/lib/gp-grid-bindings.ts +150 -0
- package/src/lib/gp-grid-view-model.ts +148 -0
- package/src/lib/gp-grid.component.ts +279 -0
- package/src/lib/gp-grid.factory.ts +52 -0
- package/src/lib/gp-grid.template.ts +77 -0
- package/src/lib/styles/index.ts +0 -0
- package/src/lib/types.ts +28 -0
- package/src/public-api.ts +57 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lib.json +27 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Signal, computed, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
calculateFillHandlePosition,
|
|
4
|
+
calculateScaledColumnPositions,
|
|
5
|
+
getTotalWidth,
|
|
6
|
+
} from '@gp-grid/core';
|
|
7
|
+
import type {
|
|
8
|
+
BatchChangeSetters,
|
|
9
|
+
CellPosition,
|
|
10
|
+
CellRange,
|
|
11
|
+
ColumnDefinition,
|
|
12
|
+
DragState,
|
|
13
|
+
FillHandlePosition,
|
|
14
|
+
FilterPopupState,
|
|
15
|
+
HeaderData,
|
|
16
|
+
SlotData,
|
|
17
|
+
VisibleColumnInfo,
|
|
18
|
+
} from '@gp-grid/core';
|
|
19
|
+
import type { ActiveFilterPopup, EditingCellState } from './components';
|
|
20
|
+
import type { AngularColumnDefinition } from './types';
|
|
21
|
+
|
|
22
|
+
export interface GpGridViewModelDeps {
|
|
23
|
+
getColumns: () => AngularColumnDefinition[];
|
|
24
|
+
getRows: () => unknown[];
|
|
25
|
+
getRowHeight: () => number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const INITIAL_DRAG_STATE: DragState = {
|
|
29
|
+
isDragging: false,
|
|
30
|
+
dragType: null,
|
|
31
|
+
fillSourceRange: null,
|
|
32
|
+
fillTarget: null,
|
|
33
|
+
columnResize: null,
|
|
34
|
+
columnMove: null,
|
|
35
|
+
rowDrag: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reactive view-state container for GpGridComponent.
|
|
40
|
+
*
|
|
41
|
+
* Owns every signal and computed the template binds against, plus the
|
|
42
|
+
* batch-change setters bag wired into those signals. The component
|
|
43
|
+
* becomes a thin shell that holds lifecycle, event handlers, and inputs.
|
|
44
|
+
*
|
|
45
|
+
* Angular-specific (uses signal/computed from @angular/core) — lives in
|
|
46
|
+
* the angular package, not core.
|
|
47
|
+
*/
|
|
48
|
+
export class GpGridViewModel {
|
|
49
|
+
readonly headerState = signal<Map<number, HeaderData>>(new Map());
|
|
50
|
+
readonly viewportWidth = signal<number>(0);
|
|
51
|
+
readonly scrollLeft = signal<number>(0);
|
|
52
|
+
readonly isLoading = signal<boolean>(false);
|
|
53
|
+
readonly errorMessage = signal<string | null>(null);
|
|
54
|
+
readonly filterPopup = signal<ActiveFilterPopup | null>(null);
|
|
55
|
+
readonly pendingScrollTop = signal<number | null>(null);
|
|
56
|
+
readonly activeCell = signal<CellPosition | null>(null);
|
|
57
|
+
readonly selectionRange = signal<CellRange | null>(null);
|
|
58
|
+
readonly editingCell = signal<EditingCellState | null>(null);
|
|
59
|
+
readonly hoverPosition = signal<CellPosition | null>(null);
|
|
60
|
+
readonly columnsOverride = signal<ColumnDefinition[] | null>(null);
|
|
61
|
+
readonly dragState = signal<DragState>(INITIAL_DRAG_STATE);
|
|
62
|
+
readonly contentWidth = signal<number>(0);
|
|
63
|
+
readonly contentHeight = signal<number>(0);
|
|
64
|
+
readonly rowsWrapperOffset = signal<number>(0);
|
|
65
|
+
readonly slots = signal<Map<string, SlotData>>(new Map());
|
|
66
|
+
|
|
67
|
+
readonly effectiveColumns: Signal<ColumnDefinition[]>;
|
|
68
|
+
readonly visibleColumnWithIndices: Signal<VisibleColumnInfo[]>;
|
|
69
|
+
readonly columnPositions: Signal<number[]>;
|
|
70
|
+
readonly columnWidths: Signal<number[]>;
|
|
71
|
+
readonly totalWidth: Signal<number>;
|
|
72
|
+
readonly fillHandlePosition: Signal<FillHandlePosition | null>;
|
|
73
|
+
readonly slotsArray: Signal<SlotData[]>;
|
|
74
|
+
readonly totalRows: Signal<number>;
|
|
75
|
+
|
|
76
|
+
readonly batchSetters: BatchChangeSetters;
|
|
77
|
+
|
|
78
|
+
private filterAnchorEl: HTMLElement | null = null;
|
|
79
|
+
|
|
80
|
+
constructor(deps: GpGridViewModelDeps) {
|
|
81
|
+
this.effectiveColumns = computed(() =>
|
|
82
|
+
this.columnsOverride() ?? (deps.getColumns() as unknown as ColumnDefinition[])
|
|
83
|
+
);
|
|
84
|
+
this.visibleColumnWithIndices = computed(() =>
|
|
85
|
+
this.effectiveColumns()
|
|
86
|
+
.map((col, index) => ({ column: col, originalIndex: index }))
|
|
87
|
+
.filter(({ column }) => !column.hidden)
|
|
88
|
+
);
|
|
89
|
+
const columnLayout = computed(() =>
|
|
90
|
+
calculateScaledColumnPositions(
|
|
91
|
+
this.visibleColumnWithIndices().map(v => v.column),
|
|
92
|
+
this.viewportWidth(),
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
this.columnPositions = computed(() => columnLayout().positions);
|
|
96
|
+
this.columnWidths = computed(() => columnLayout().widths);
|
|
97
|
+
this.totalWidth = computed(() => getTotalWidth(this.columnPositions()));
|
|
98
|
+
this.fillHandlePosition = computed(() =>
|
|
99
|
+
calculateFillHandlePosition({
|
|
100
|
+
activeCell: this.activeCell(),
|
|
101
|
+
selectionRange: this.selectionRange(),
|
|
102
|
+
slots: this.slots(),
|
|
103
|
+
columns: this.effectiveColumns(),
|
|
104
|
+
visibleColumnsWithIndices: this.visibleColumnWithIndices(),
|
|
105
|
+
columnPositions: this.columnPositions(),
|
|
106
|
+
columnWidths: this.columnWidths(),
|
|
107
|
+
rowHeight: deps.getRowHeight(),
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
this.slotsArray = computed(() => [...this.slots().values()]);
|
|
111
|
+
this.totalRows = computed(() => deps.getRows().length);
|
|
112
|
+
|
|
113
|
+
this.batchSetters = {
|
|
114
|
+
setContentWidth: (v) => this.contentWidth.set(v),
|
|
115
|
+
setContentHeight: (v) => this.contentHeight.set(v),
|
|
116
|
+
setRowsWrapperOffset: (v) => this.rowsWrapperOffset.set(v),
|
|
117
|
+
setIsLoading: (v) => this.isLoading.set(v),
|
|
118
|
+
setErrorMessage: (v) => this.errorMessage.set(v),
|
|
119
|
+
setPendingScrollTop: (v) => this.pendingScrollTop.set(v),
|
|
120
|
+
setActiveCell: (v) => this.activeCell.set(v),
|
|
121
|
+
setSelectionRange: (v) => this.selectionRange.set(v),
|
|
122
|
+
setEditingCell: (v) => this.editingCell.set(v),
|
|
123
|
+
setHoverPosition: (v) => this.hoverPosition.set(v),
|
|
124
|
+
setColumnsOverride: (v) => this.columnsOverride.set(v),
|
|
125
|
+
onFilterPopupChange: (v) => this.materializeFilterPopup(v),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setFilterAnchor(el: HTMLElement | null): void {
|
|
130
|
+
this.filterAnchorEl = el;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private materializeFilterPopup(state: FilterPopupState | null): void {
|
|
134
|
+
if (state === null) {
|
|
135
|
+
this.filterPopup.set(null);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (state.isOpen && state.column) {
|
|
139
|
+
this.filterPopup.set({
|
|
140
|
+
colIndex: state.colIndex,
|
|
141
|
+
column: state.column,
|
|
142
|
+
distinctValues: state.distinctValues,
|
|
143
|
+
currentFilter: state.currentFilter,
|
|
144
|
+
anchorEl: this.filterAnchorEl,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ElementRef,
|
|
4
|
+
OnInit,
|
|
5
|
+
AfterViewInit,
|
|
6
|
+
OnDestroy,
|
|
7
|
+
ViewChild,
|
|
8
|
+
ChangeDetectionStrategy,
|
|
9
|
+
PLATFORM_ID,
|
|
10
|
+
inject,
|
|
11
|
+
input,
|
|
12
|
+
output,
|
|
13
|
+
effect,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import type { CellRendererTemplate, EditRendererTemplate, HeaderRendererTemplate, HeaderSortEvent } from './components';
|
|
16
|
+
import type { AngularColumnDefinition } from './types';
|
|
17
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
18
|
+
import type {
|
|
19
|
+
CellValueChangedEvent,
|
|
20
|
+
ColumnDefinition,
|
|
21
|
+
ColumnFilterModel,
|
|
22
|
+
DataSource,
|
|
23
|
+
HighlightingOptions,
|
|
24
|
+
RowId,
|
|
25
|
+
} from '@gp-grid/core';
|
|
26
|
+
import {
|
|
27
|
+
GridHeaderComponent,
|
|
28
|
+
GridBodyComponent,
|
|
29
|
+
GridOverlaysComponent,
|
|
30
|
+
} from './components';
|
|
31
|
+
import type {
|
|
32
|
+
HeaderPointerDownEvent,
|
|
33
|
+
FilterPointerDownEvent,
|
|
34
|
+
ResizePointerDownEvent,
|
|
35
|
+
CellPointerDownEvent,
|
|
36
|
+
CellPointerEnterEvent,
|
|
37
|
+
CellDoubleClickEvent,
|
|
38
|
+
FillHandlePointerDownEvent,
|
|
39
|
+
} from './components';
|
|
40
|
+
import { GP_GRID_TEMPLATE } from './gp-grid.template';
|
|
41
|
+
import { GpGridViewModel } from './gp-grid-view-model';
|
|
42
|
+
import { GpGridBindings } from './gp-grid-bindings';
|
|
43
|
+
import { buildGridCore } from './gp-grid.factory';
|
|
44
|
+
|
|
45
|
+
@Component({
|
|
46
|
+
selector: 'gp-grid',
|
|
47
|
+
standalone: true,
|
|
48
|
+
imports: [GridHeaderComponent, GridBodyComponent, GridOverlaysComponent],
|
|
49
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
50
|
+
styles: [`:host { display: block; height: 100%; min-height: 0; }`],
|
|
51
|
+
template: GP_GRID_TEMPLATE,
|
|
52
|
+
})
|
|
53
|
+
export class GpGridComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
54
|
+
@ViewChild('container', { static: true }) container!: ElementRef<HTMLDivElement>;
|
|
55
|
+
@ViewChild(GridBodyComponent) body!: GridBodyComponent;
|
|
56
|
+
|
|
57
|
+
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
58
|
+
|
|
59
|
+
columns = input.required<AngularColumnDefinition[]>();
|
|
60
|
+
rows = input<unknown[]>([]);
|
|
61
|
+
dataSource = input<DataSource<unknown> | null>(null);
|
|
62
|
+
getRowId = input<((row: unknown) => RowId) | null>(null);
|
|
63
|
+
rowHeight = input<number>(32);
|
|
64
|
+
headerHeight = input<number>(32);
|
|
65
|
+
darkMode = input<boolean>(false);
|
|
66
|
+
cellRenderers = input<Record<string, CellRendererTemplate>>({});
|
|
67
|
+
headerRenderers = input<Record<string, HeaderRendererTemplate>>({});
|
|
68
|
+
editRenderers = input<Record<string, EditRendererTemplate>>({});
|
|
69
|
+
cellRenderer = input<CellRendererTemplate | null>(null);
|
|
70
|
+
headerRenderer = input<HeaderRendererTemplate | null>(null);
|
|
71
|
+
editRenderer = input<EditRendererTemplate | null>(null);
|
|
72
|
+
highlighting = input<HighlightingOptions | null>(null);
|
|
73
|
+
rowDragEntireRow = input<boolean>(false);
|
|
74
|
+
overscan = input<number>(3);
|
|
75
|
+
sortingEnabled = input<boolean>(true);
|
|
76
|
+
wheelDampening = input<number>(0.1);
|
|
77
|
+
onRowDragEnd = output<{ source: number; target: number }>();
|
|
78
|
+
onCellValueChanged = output<CellValueChangedEvent<unknown>>();
|
|
79
|
+
onColumnResized = output<{ colIndex: number; newWidth: number }>();
|
|
80
|
+
onColumnMoved = output<{ fromIndex: number; toIndex: number }>();
|
|
81
|
+
|
|
82
|
+
protected readonly vm = new GpGridViewModel({
|
|
83
|
+
getColumns: () => this.columns(),
|
|
84
|
+
getRows: () => this.rows(),
|
|
85
|
+
getRowHeight: () => this.rowHeight(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
private readonly bindings = new GpGridBindings<unknown>({
|
|
89
|
+
vm: this.vm,
|
|
90
|
+
isBrowser: this.isBrowser,
|
|
91
|
+
getContainer: () => this.container?.nativeElement ?? null,
|
|
92
|
+
getBody: () => this.body?.scrollContainer?.nativeElement ?? null,
|
|
93
|
+
getRowHeight: () => this.rowHeight(),
|
|
94
|
+
getHeaderHeight: () => this.headerHeight(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
effect(() => this.bindings.applyPendingScroll());
|
|
99
|
+
effect(() => this.bindings.syncHighlighting(this.highlighting()));
|
|
100
|
+
effect(() => this.bindings.syncColumns(this.columns() as unknown as ColumnDefinition[]));
|
|
101
|
+
effect(() => this.bindings.syncRows(this.rows(), this.dataSource()));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ngOnInit(): void {
|
|
105
|
+
const core = buildGridCore<unknown>(
|
|
106
|
+
{
|
|
107
|
+
columns: this.columns() as unknown as ColumnDefinition[],
|
|
108
|
+
dataSource: this.bindings.dataSourceOwner.initialize(this.dataSource(), this.rows()),
|
|
109
|
+
rowHeight: this.rowHeight(),
|
|
110
|
+
headerHeight: this.headerHeight(),
|
|
111
|
+
overscan: this.overscan(),
|
|
112
|
+
sortingEnabled: this.sortingEnabled(),
|
|
113
|
+
highlighting: (this.highlighting() ?? undefined) as HighlightingOptions<unknown> | undefined,
|
|
114
|
+
getRowId: this.getRowId() ?? undefined,
|
|
115
|
+
rowDragEntireRow: this.rowDragEntireRow(),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
onRowDragEnd: (source, target) => this.onRowDragEnd.emit({ source, target }),
|
|
119
|
+
onCellValueChanged: (event) => this.onCellValueChanged.emit(event),
|
|
120
|
+
onColumnResized: (colIndex, newWidth) => this.onColumnResized.emit({ colIndex, newWidth }),
|
|
121
|
+
onColumnMoved: (fromIndex, toIndex) => this.onColumnMoved.emit({ fromIndex, toIndex }),
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
this.bindings.attach(core);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ngAfterViewInit(): void {
|
|
128
|
+
if (this.isBrowser === false) return;
|
|
129
|
+
this.bindings.observeViewport(
|
|
130
|
+
this.container.nativeElement,
|
|
131
|
+
this.body.scrollContainer.nativeElement,
|
|
132
|
+
);
|
|
133
|
+
document.addEventListener('pointermove', this.onDocumentPointerMove, { passive: false });
|
|
134
|
+
document.addEventListener('pointerup', this.onDocumentPointerUp);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ngOnDestroy(): void {
|
|
138
|
+
this.bindings.destroy();
|
|
139
|
+
if (this.isBrowser) {
|
|
140
|
+
document.removeEventListener('pointermove', this.onDocumentPointerMove);
|
|
141
|
+
document.removeEventListener('pointerup', this.onDocumentPointerUp);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected onBodyScroll(scrollLeft: number): void {
|
|
146
|
+
this.vm.scrollLeft.set(scrollLeft);
|
|
147
|
+
const el = this.body.scrollContainer.nativeElement;
|
|
148
|
+
this.bindings.coreRef?.setViewport(el.scrollTop, scrollLeft, el.clientWidth, el.clientHeight);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected onHeaderPointerDown(evt: HeaderPointerDownEvent): void {
|
|
152
|
+
if (this.bindings.input.headerPointerDown(evt.colIndex, evt.colWidth, evt.colHeight, evt.event)) {
|
|
153
|
+
evt.event.preventDefault();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected onFilterPointerDown(evt: FilterPointerDownEvent): void {
|
|
158
|
+
this.vm.setFilterAnchor(evt.anchorEl);
|
|
159
|
+
const rect = evt.anchorEl.getBoundingClientRect();
|
|
160
|
+
this.bindings.coreRef?.openFilterPopup(evt.colIndex, {
|
|
161
|
+
top: rect.top,
|
|
162
|
+
left: rect.left,
|
|
163
|
+
width: rect.width,
|
|
164
|
+
height: rect.height,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected onCellPointerDown(evt: CellPointerDownEvent): void {
|
|
169
|
+
const action = this.bindings.input.cellPointerDown(evt.rowIndex, evt.colIndex, evt.event);
|
|
170
|
+
if (action.preventDefault) evt.event.preventDefault();
|
|
171
|
+
if (action.focusContainer) {
|
|
172
|
+
this.container.nativeElement.focus({ preventScroll: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected onCellPointerEnter(evt: CellPointerEnterEvent): void {
|
|
177
|
+
this.bindings.input.cellPointerEnter(evt.rowIndex, evt.colIndex);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
protected onFillHandlePointerDown(evt: FillHandlePointerDownEvent): void {
|
|
181
|
+
const action = this.bindings.input.fillHandlePointerDown(
|
|
182
|
+
this.vm.activeCell(),
|
|
183
|
+
this.vm.selectionRange(),
|
|
184
|
+
evt.event,
|
|
185
|
+
);
|
|
186
|
+
if (action.preventDefault) evt.event.preventDefault();
|
|
187
|
+
if (action.stopPropagation) evt.event.stopPropagation();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
protected onCellPointerLeave(): void {
|
|
191
|
+
this.bindings.input.cellPointerLeave();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
protected computeRowClassesFn = (rowIndex: number, rowData: unknown): string[] => {
|
|
195
|
+
return this.bindings.coreRef?.highlight?.computeRowClasses(rowIndex, rowData) ?? [];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
protected computeCellClassesFn = (
|
|
199
|
+
rowIndex: number,
|
|
200
|
+
colIndex: number,
|
|
201
|
+
column: ColumnDefinition,
|
|
202
|
+
rowData: unknown,
|
|
203
|
+
): string[] => {
|
|
204
|
+
return this.bindings.coreRef?.highlight?.computeCombinedCellClasses(
|
|
205
|
+
rowIndex,
|
|
206
|
+
colIndex,
|
|
207
|
+
column,
|
|
208
|
+
rowData,
|
|
209
|
+
) ?? [];
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
protected onCellDoubleClick(evt: CellDoubleClickEvent): void {
|
|
213
|
+
this.bindings.coreRef?.startEdit(evt.rowIndex, evt.colIndex);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected onEditValueChange(value: string): void {
|
|
217
|
+
this.bindings.coreRef?.updateEditValue(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
protected onEditCommit(): void {
|
|
221
|
+
this.bindings.coreRef?.commitEdit();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
protected onEditCancel(): void {
|
|
225
|
+
this.bindings.coreRef?.cancelEdit();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected onHeaderSort(evt: HeaderSortEvent): void {
|
|
229
|
+
this.bindings.coreRef?.setSort(evt.colId, evt.direction, evt.addToExisting);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected onWheel(event: WheelEvent): void {
|
|
233
|
+
const bodyEl = this.body?.scrollContainer?.nativeElement;
|
|
234
|
+
if (!bodyEl) return;
|
|
235
|
+
const dampened = this.bindings.input.wheel(event.deltaY, event.deltaX, this.wheelDampening());
|
|
236
|
+
if (dampened) {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
bodyEl.scrollTop += dampened.dy;
|
|
239
|
+
bodyEl.scrollLeft += dampened.dx;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected onKeyDown(event: KeyboardEvent): void {
|
|
244
|
+
const editing = this.vm.editingCell();
|
|
245
|
+
const result = this.bindings.input.keyDown(
|
|
246
|
+
event,
|
|
247
|
+
this.vm.activeCell(),
|
|
248
|
+
editing === null ? null : { row: editing.row, col: editing.col },
|
|
249
|
+
this.vm.filterPopup() !== null,
|
|
250
|
+
);
|
|
251
|
+
if (result.preventDefault) event.preventDefault();
|
|
252
|
+
if (result.scrollToCell) this.bindings.scrollToRow(result.scrollToCell.row);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected onResizePointerDown(evt: ResizePointerDownEvent): void {
|
|
256
|
+
if (this.bindings.input.resizePointerDown(evt.colIndex, evt.colWidth, evt.event)) {
|
|
257
|
+
evt.event.preventDefault();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
protected onFilterApply(event: { colId: string; filter: ColumnFilterModel | null }): void {
|
|
262
|
+
this.bindings.coreRef?.setFilter(event.colId, event.filter);
|
|
263
|
+
this.vm.filterPopup.set(null);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
protected onFilterClose(): void {
|
|
267
|
+
this.bindings.coreRef?.closeFilterPopup();
|
|
268
|
+
this.vm.filterPopup.set(null);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private onDocumentPointerMove = (event: PointerEvent): void => {
|
|
272
|
+
if (this.bindings.input.documentPointerMove(event)) event.preventDefault();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
private onDocumentPointerUp = (_event: PointerEvent): void => {
|
|
276
|
+
const { wasRowDrag } = this.bindings.input.documentPointerUp();
|
|
277
|
+
if (wasRowDrag) this.bindings.pendingRowDrag.releaseLocks();
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { GridCore } from '@gp-grid/core';
|
|
2
|
+
import type {
|
|
3
|
+
CellValueChangedEvent,
|
|
4
|
+
ColumnDefinition,
|
|
5
|
+
DataSource,
|
|
6
|
+
HighlightingOptions,
|
|
7
|
+
RowId,
|
|
8
|
+
} from '@gp-grid/core';
|
|
9
|
+
|
|
10
|
+
export interface BuildGridCoreInputs<TData> {
|
|
11
|
+
columns: ColumnDefinition[];
|
|
12
|
+
dataSource: DataSource<TData>;
|
|
13
|
+
rowHeight: number;
|
|
14
|
+
headerHeight: number;
|
|
15
|
+
overscan: number;
|
|
16
|
+
sortingEnabled: boolean;
|
|
17
|
+
highlighting: HighlightingOptions<TData> | undefined;
|
|
18
|
+
getRowId: ((row: TData) => RowId) | undefined;
|
|
19
|
+
rowDragEntireRow: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BuildGridCoreEmitters<TData> {
|
|
23
|
+
onRowDragEnd: (source: number, target: number) => void;
|
|
24
|
+
onCellValueChanged: (event: CellValueChangedEvent<TData>) => void;
|
|
25
|
+
onColumnResized: (colIndex: number, newWidth: number) => void;
|
|
26
|
+
onColumnMoved: (fromIndex: number, toIndex: number) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const buildGridCore = <TData>(
|
|
30
|
+
inputs: BuildGridCoreInputs<TData>,
|
|
31
|
+
emitters: BuildGridCoreEmitters<TData>,
|
|
32
|
+
): GridCore<TData> => {
|
|
33
|
+
const cellValueChanged = inputs.getRowId === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: emitters.onCellValueChanged;
|
|
36
|
+
|
|
37
|
+
return new GridCore<TData>({
|
|
38
|
+
columns: inputs.columns,
|
|
39
|
+
dataSource: inputs.dataSource,
|
|
40
|
+
rowHeight: inputs.rowHeight,
|
|
41
|
+
headerHeight: inputs.headerHeight,
|
|
42
|
+
overscan: inputs.overscan,
|
|
43
|
+
sortingEnabled: inputs.sortingEnabled,
|
|
44
|
+
highlighting: inputs.highlighting,
|
|
45
|
+
getRowId: inputs.getRowId,
|
|
46
|
+
rowDragEntireRow: inputs.rowDragEntireRow,
|
|
47
|
+
onRowDragEnd: emitters.onRowDragEnd,
|
|
48
|
+
onCellValueChanged: cellValueChanged,
|
|
49
|
+
onColumnResized: emitters.onColumnResized,
|
|
50
|
+
onColumnMoved: emitters.onColumnMoved,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const GP_GRID_TEMPLATE = `
|
|
2
|
+
<div #container
|
|
3
|
+
[class]="'gp-grid-container' + (darkMode() ? ' gp-grid-container--dark' : '')"
|
|
4
|
+
style="width: 100%; height: 100%; display: flex; flex-direction: column; position: relative; outline: none;"
|
|
5
|
+
tabindex="0"
|
|
6
|
+
(keydown)="onKeyDown($event)"
|
|
7
|
+
(wheel)="onWheel($event)"
|
|
8
|
+
>
|
|
9
|
+
<gp-grid-header
|
|
10
|
+
[headerHeight]="headerHeight()"
|
|
11
|
+
[scrollLeft]="vm.scrollLeft()"
|
|
12
|
+
[contentWidth]="vm.contentWidth()"
|
|
13
|
+
[totalWidth]="vm.totalWidth()"
|
|
14
|
+
[isLoading]="vm.isLoading()"
|
|
15
|
+
[visibleColumnsWithIndices]="vm.visibleColumnWithIndices()"
|
|
16
|
+
[columnPositions]="vm.columnPositions()"
|
|
17
|
+
[columnWidths]="vm.columnWidths()"
|
|
18
|
+
[headers]="vm.headerState()"
|
|
19
|
+
[sortingEnabled]="sortingEnabled()"
|
|
20
|
+
[headerRenderers]="headerRenderers()"
|
|
21
|
+
[globalHeaderRenderer]="headerRenderer()"
|
|
22
|
+
(headerPointerDown)="onHeaderPointerDown($event)"
|
|
23
|
+
(filterPointerDown)="onFilterPointerDown($event)"
|
|
24
|
+
(resizePointerDown)="onResizePointerDown($event)"
|
|
25
|
+
(headerSort)="onHeaderSort($event)"
|
|
26
|
+
/>
|
|
27
|
+
<gp-grid-body
|
|
28
|
+
[rowHeight]="rowHeight()"
|
|
29
|
+
[totalHeaderHeight]="headerHeight()"
|
|
30
|
+
[contentWidth]="vm.contentWidth()"
|
|
31
|
+
[contentHeight]="vm.contentHeight()"
|
|
32
|
+
[totalWidth]="vm.totalWidth()"
|
|
33
|
+
[rowsWrapperOffset]="vm.rowsWrapperOffset()"
|
|
34
|
+
[slotsArray]="vm.slotsArray()"
|
|
35
|
+
[visibleColumnWithIndices]="vm.visibleColumnWithIndices()"
|
|
36
|
+
[columnPositions]="vm.columnPositions()"
|
|
37
|
+
[columnWidths]="vm.columnWidths()"
|
|
38
|
+
[totalRows]="vm.totalRows()"
|
|
39
|
+
[activeCell]="vm.activeCell()"
|
|
40
|
+
[selectionRange]="vm.selectionRange()"
|
|
41
|
+
[editingCell]="vm.editingCell()"
|
|
42
|
+
[cellRenderers]="cellRenderers()"
|
|
43
|
+
[globalCellRenderer]="cellRenderer()"
|
|
44
|
+
[editRenderers]="editRenderers()"
|
|
45
|
+
[globalEditRenderer]="editRenderer()"
|
|
46
|
+
[hoverPosition]="vm.hoverPosition()"
|
|
47
|
+
[computeRowClasses]="computeRowClassesFn"
|
|
48
|
+
[computeCellClasses]="computeCellClassesFn"
|
|
49
|
+
[fillHandlePosition]="vm.fillHandlePosition()"
|
|
50
|
+
[dragState]="vm.dragState()"
|
|
51
|
+
(scrolled)="onBodyScroll($event)"
|
|
52
|
+
(cellPointerDown)="onCellPointerDown($event)"
|
|
53
|
+
(cellPointerEnter)="onCellPointerEnter($event)"
|
|
54
|
+
(cellPointerLeave)="onCellPointerLeave()"
|
|
55
|
+
(cellDoubleClick)="onCellDoubleClick($event)"
|
|
56
|
+
(editValueChange)="onEditValueChange($event)"
|
|
57
|
+
(editCommit)="onEditCommit()"
|
|
58
|
+
(editCancel)="onEditCancel()"
|
|
59
|
+
(fillHandlePointerDown)="onFillHandlePointerDown($event)"
|
|
60
|
+
/>
|
|
61
|
+
<gp-grid-overlays
|
|
62
|
+
[filterPopup]="vm.filterPopup()"
|
|
63
|
+
[isLoading]="vm.isLoading()"
|
|
64
|
+
[errorMessage]="vm.errorMessage()"
|
|
65
|
+
[headerHeight]="headerHeight()"
|
|
66
|
+
[rowHeight]="rowHeight()"
|
|
67
|
+
[dragState]="vm.dragState()"
|
|
68
|
+
[visibleColumnWithIndices]="vm.visibleColumnWithIndices()"
|
|
69
|
+
[columnPositions]="vm.columnPositions()"
|
|
70
|
+
[scrollLeft]="vm.scrollLeft()"
|
|
71
|
+
[effectiveColumns]="vm.effectiveColumns()"
|
|
72
|
+
[totalWidth]="vm.totalWidth()"
|
|
73
|
+
(filterApply)="onFilterApply($event)"
|
|
74
|
+
(filterClose)="onFilterClose()"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
File without changes
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TemplateRef } from '@angular/core';
|
|
2
|
+
import type {
|
|
3
|
+
ColumnDefinition,
|
|
4
|
+
CellRendererParams,
|
|
5
|
+
EditRendererParams,
|
|
6
|
+
HeaderRendererParams,
|
|
7
|
+
} from '@gp-grid/core';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Column definition extended for Angular — allows `cellRenderer`,
|
|
11
|
+
* `editRenderer`, and `headerRenderer` to be passed directly as
|
|
12
|
+
* `TemplateRef` references in addition to the core's string-key / function
|
|
13
|
+
* forms.
|
|
14
|
+
*/
|
|
15
|
+
export interface AngularColumnDefinition extends Omit<ColumnDefinition, 'cellRenderer' | 'editRenderer' | 'headerRenderer'> {
|
|
16
|
+
cellRenderer?:
|
|
17
|
+
| string
|
|
18
|
+
| TemplateRef<{ $implicit: CellRendererParams }>
|
|
19
|
+
| ((params: CellRendererParams) => unknown);
|
|
20
|
+
editRenderer?:
|
|
21
|
+
| string
|
|
22
|
+
| TemplateRef<{ $implicit: EditRendererParams }>
|
|
23
|
+
| ((params: EditRendererParams) => unknown);
|
|
24
|
+
headerRenderer?:
|
|
25
|
+
| string
|
|
26
|
+
| TemplateRef<{ $implicit: HeaderRendererParams }>
|
|
27
|
+
| ((params: HeaderRendererParams) => unknown);
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export * from './lib/gp-grid.component';
|
|
2
|
+
export * from './lib/components';
|
|
3
|
+
export * from './lib/types';
|
|
4
|
+
export * from './lib/createGridData';
|
|
5
|
+
|
|
6
|
+
// Re-export core types for convenience
|
|
7
|
+
export type {
|
|
8
|
+
// Basic types
|
|
9
|
+
CellDataType,
|
|
10
|
+
CellValue,
|
|
11
|
+
SortDirection,
|
|
12
|
+
SortModel,
|
|
13
|
+
FilterModel,
|
|
14
|
+
FilterCondition,
|
|
15
|
+
ColumnFilterModel,
|
|
16
|
+
|
|
17
|
+
// Column definition
|
|
18
|
+
ColumnDefinition,
|
|
19
|
+
|
|
20
|
+
// Row ID
|
|
21
|
+
RowId,
|
|
22
|
+
|
|
23
|
+
// Cell position & range
|
|
24
|
+
CellPosition,
|
|
25
|
+
CellRange,
|
|
26
|
+
|
|
27
|
+
// Events
|
|
28
|
+
CellValueChangedEvent,
|
|
29
|
+
|
|
30
|
+
// DataSource
|
|
31
|
+
DataSource,
|
|
32
|
+
DataSourceRequest,
|
|
33
|
+
DataSourceResponse,
|
|
34
|
+
|
|
35
|
+
// Renderer params
|
|
36
|
+
CellRendererParams,
|
|
37
|
+
EditRendererParams,
|
|
38
|
+
HeaderRendererParams,
|
|
39
|
+
|
|
40
|
+
// Highlighting
|
|
41
|
+
HighlightingOptions,
|
|
42
|
+
HighlightContext,
|
|
43
|
+
|
|
44
|
+
// Instructions (for advanced use cases)
|
|
45
|
+
GridInstruction,
|
|
46
|
+
} from '@gp-grid/core';
|
|
47
|
+
|
|
48
|
+
// Re-export data source factories
|
|
49
|
+
export {
|
|
50
|
+
createClientDataSource,
|
|
51
|
+
createServerDataSource,
|
|
52
|
+
createDataSourceFromArray,
|
|
53
|
+
createMutableClientDataSource,
|
|
54
|
+
} from '@gp-grid/core';
|
|
55
|
+
|
|
56
|
+
export type { MutableDataSource } from '@gp-grid/core';
|
|
57
|
+
export { GridCore } from '@gp-grid/core';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"ignoreDeprecations": "6.0",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"lib": [
|
|
10
|
+
"ES2022",
|
|
11
|
+
"dom"
|
|
12
|
+
],
|
|
13
|
+
"outDir": "../../dist/angular",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@gp-grid/core": ["../core/src"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"angularCompilerOptions": {
|
|
19
|
+
"compilationMode": "partial"
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*.ts"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"src/**/*.spec.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|