@gp-grid/angular 0.10.3 → 0.11.2

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.
@@ -1,284 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, ElementRef, input, output, TemplateRef, ViewChild } from "@angular/core";
2
- import { NgTemplateOutlet } from "@angular/common";
3
- import {
4
- formatCellValue,
5
- getFieldValue,
6
- buildCellClasses,
7
- isCellActive,
8
- isCellEditing,
9
- isCellInFillPreview,
10
- isCellSelected,
11
- SlotData,
12
- VisibleColumnInfo,
13
- CellPosition,
14
- CellRange,
15
- CellValue,
16
- ColumnDefinition,
17
- CellRendererParams,
18
- EditRendererParams,
19
- FillHandlePosition,
20
- DragState,
21
- } from "@gp-grid/core";
22
- import { GRID_BODY_TEMPLATE } from "./grid-body.template";
23
-
24
- export type RowClassFn = (rowIndex: number, rowData: unknown) => string[];
25
- export type CellClassFn = (
26
- rowIndex: number,
27
- colIndex: number,
28
- column: ColumnDefinition,
29
- rowData: unknown,
30
- ) => string[];
31
-
32
- export type CellRendererTemplate = TemplateRef<{ $implicit: CellRendererParams }>;
33
- export type EditRendererTemplate = TemplateRef<{ $implicit: EditRendererParams }>;
34
-
35
- export interface FillHandlePointerDownEvent {
36
- event: PointerEvent;
37
- }
38
-
39
- export interface CellPointerDownEvent {
40
- rowIndex: number;
41
- colIndex: number;
42
- event: PointerEvent;
43
- }
44
-
45
- export interface CellPointerEnterEvent {
46
- rowIndex: number;
47
- colIndex: number;
48
- }
49
-
50
- export interface CellDoubleClickEvent {
51
- rowIndex: number;
52
- colIndex: number;
53
- }
54
-
55
- export interface EditingCellState {
56
- row: number;
57
- col: number;
58
- initialValue: CellValue;
59
- }
60
-
61
- @Component({
62
- selector: "gp-grid-body",
63
- standalone: true,
64
- imports: [NgTemplateOutlet],
65
- changeDetection: ChangeDetectionStrategy.OnPush,
66
- styles: [`:host { display: flex; flex: 1; min-height: 0; overflow: hidden; }`],
67
- template: GRID_BODY_TEMPLATE,
68
- })
69
- export class GridBodyComponent {
70
- @ViewChild("scrollContainer") scrollContainer!: ElementRef<HTMLDivElement>;
71
- rowHeight = input.required<number>();
72
- totalHeaderHeight = input.required<number>();
73
- contentWidth = input.required<number>();
74
- contentHeight = input.required<number>();
75
- rowsWrapperOffset = input.required<number>();
76
- slotsArray = input.required<SlotData[]>();
77
- visibleColumnWithIndices = input.required<VisibleColumnInfo[]>();
78
- totalWidth = input.required<number>();
79
- columnPositions = input.required<number[]>();
80
- columnWidths = input.required<number[]>();
81
- totalRows = input.required<number>();
82
- activeCell = input<CellPosition | null>(null);
83
- selectionRange = input<CellRange | null>(null);
84
- editingCell = input<EditingCellState | null>(null);
85
- cellRenderers = input<Record<string, CellRendererTemplate>>({});
86
- globalCellRenderer = input<CellRendererTemplate | null>(null);
87
- editRenderers = input<Record<string, EditRendererTemplate>>({});
88
- globalEditRenderer = input<EditRendererTemplate | null>(null);
89
- hoverPosition = input<CellPosition | null>(null);
90
- computeRowClasses = input<RowClassFn | null>(null);
91
- computeCellClasses = input<CellClassFn | null>(null);
92
- fillHandlePosition = input<FillHandlePosition | null>(null);
93
- dragState = input<DragState | null>(null);
94
-
95
- scrolled = output<number>();
96
- cellPointerDown = output<CellPointerDownEvent>();
97
- cellPointerEnter = output<CellPointerEnterEvent>();
98
- cellPointerLeave = output<void>();
99
- cellDoubleClick = output<CellDoubleClickEvent>();
100
- editValueChange = output<string>();
101
- editCommit = output<void>();
102
- editCancel = output<void>();
103
- fillHandlePointerDown = output<FillHandlePointerDownEvent>();
104
-
105
- protected innerWidth = computed(() =>
106
- Math.max(this.contentWidth(), this.totalWidth()),
107
- );
108
-
109
- protected sizerHeight = computed(() =>
110
- Math.max(this.contentHeight() - this.totalHeaderHeight(), 0),
111
- );
112
-
113
- protected rowDropIndicator = computed(() => {
114
- const ds = this.dragState();
115
- if (ds?.dragType !== 'row-drag') return null;
116
- if (ds.rowDrag === null || ds.rowDrag.dropTargetIndex === null) return null;
117
- return ds.rowDrag;
118
- });
119
-
120
- protected rowDropIndicatorWidth = computed(() =>
121
- Math.max(this.contentWidth(), this.totalWidth()),
122
- );
123
-
124
- protected wrapperTransform = computed(() =>
125
- `translateY(${this.rowsWrapperOffset()}px)`);
126
-
127
- protected onScroll(): void {
128
- const el = this.scrollContainer.nativeElement;
129
- this.scrolled.emit(el.scrollLeft);
130
- }
131
-
132
- protected cellParams(
133
- rowData: unknown,
134
- column: ColumnDefinition,
135
- rowIndex: number,
136
- colIndex: number,
137
- ): CellRendererParams {
138
- return {
139
- value: getFieldValue(rowData, column.field),
140
- rowData,
141
- column,
142
- rowIndex,
143
- colIndex,
144
- isActive: isCellActive(rowIndex, colIndex, this.activeCell()),
145
- isSelected: isCellSelected(rowIndex, colIndex, this.selectionRange()),
146
- isEditing: false,
147
- };
148
- }
149
-
150
- protected cellTemplate(column: ColumnDefinition): CellRendererTemplate | null {
151
- const renderer: unknown = column.cellRenderer;
152
- if (renderer instanceof TemplateRef) {
153
- return renderer as CellRendererTemplate;
154
- }
155
- if (typeof renderer === 'string') {
156
- const registered = this.cellRenderers()[renderer];
157
- if (registered) return registered;
158
- }
159
- return this.globalCellRenderer();
160
- }
161
-
162
- protected editTemplate(column: ColumnDefinition): EditRendererTemplate | null {
163
- const renderer: unknown = (column as { editRenderer?: unknown }).editRenderer;
164
- if (renderer instanceof TemplateRef) {
165
- return renderer as EditRendererTemplate;
166
- }
167
- if (typeof renderer === 'string') {
168
- const registered = this.editRenderers()[renderer];
169
- if (registered) return registered;
170
- }
171
- return this.globalEditRenderer();
172
- }
173
-
174
- protected editParams(
175
- rowData: unknown,
176
- column: ColumnDefinition,
177
- rowIndex: number,
178
- colIndex: number,
179
- ): EditRendererParams {
180
- const ec = this.editingCell();
181
- return {
182
- value: getFieldValue(rowData, column.field),
183
- rowData,
184
- column,
185
- rowIndex,
186
- colIndex,
187
- isActive: true,
188
- isSelected: true,
189
- isEditing: true,
190
- initialValue: ec?.initialValue ?? null,
191
- onValueChange: (newValue) => {
192
- const s = newValue === null || newValue === undefined ? '' : String(newValue);
193
- this.editValueChange.emit(s);
194
- },
195
- onCommit: () => this.editCommit.emit(),
196
- onCancel: () => this.editCancel.emit(),
197
- };
198
- }
199
-
200
- protected cellDisplay(
201
- rowData: unknown,
202
- column: ColumnDefinition,
203
- rowIndex: number,
204
- colIndex: number,
205
- ): string {
206
- const renderer = column.cellRenderer;
207
- const value = getFieldValue(rowData, column.field);
208
- if (typeof renderer === 'function') {
209
- const params = this.cellParams(rowData, column, rowIndex, colIndex);
210
- const result = renderer(params);
211
- return result === null || result === undefined ? '' : String(result);
212
- }
213
- return formatCellValue(value, column.valueFormatter);
214
- }
215
-
216
- protected cellClass(
217
- rowIndex: number,
218
- colIndex: number,
219
- column: ColumnDefinition,
220
- rowData: unknown,
221
- ): string {
222
- const editingCell = this.editingCell();
223
- // Read hoverPosition to register this signal as a dep so Angular re-renders on hover change.
224
- this.hoverPosition();
225
- const ds = this.dragState();
226
- const inFillPreview = isCellInFillPreview(
227
- rowIndex,
228
- colIndex,
229
- ds?.dragType === "fill",
230
- ds?.fillSourceRange ?? null,
231
- ds?.fillTarget ?? null,
232
- );
233
- const base = buildCellClasses(
234
- isCellActive(rowIndex, colIndex, this.activeCell()),
235
- isCellSelected(rowIndex, colIndex, this.selectionRange()),
236
- isCellEditing(rowIndex, colIndex, editingCell),
237
- inFillPreview,
238
- );
239
- const withHandle = column.rowDrag === true
240
- ? `${base} gp-grid-cell--row-drag-handle`
241
- : base;
242
- const fn = this.computeCellClasses();
243
- if (fn === null) return withHandle;
244
- const extra = fn(rowIndex, colIndex, column, rowData);
245
- if (extra.length === 0) return withHandle;
246
- return `${withHandle} ${extra.join(' ')}`;
247
- }
248
-
249
- protected rowClass(rowIndex: number, rowData: unknown): string {
250
- this.hoverPosition();
251
- const fn = this.computeRowClasses();
252
- if (fn === null) return 'gp-grid-row';
253
- const extra = fn(rowIndex, rowData);
254
- if (extra.length === 0) return 'gp-grid-row';
255
- return `gp-grid-row ${extra.join(' ')}`;
256
- }
257
-
258
- protected isEditing(rowIndex: number, colIndex: number): boolean {
259
- return isCellEditing(rowIndex, colIndex, this.editingCell());
260
- }
261
-
262
- protected editInitialValue(): string {
263
- const ec = this.editingCell();
264
- if (ec === null || ec.initialValue === null || ec.initialValue === undefined) return '';
265
- return String(ec.initialValue);
266
- }
267
-
268
- protected asInput(event: Event): HTMLInputElement {
269
- return event.target as HTMLInputElement;
270
- }
271
-
272
- protected onEditFocus(event: FocusEvent): void {
273
- (event.target as HTMLInputElement).select();
274
- }
275
-
276
- protected onEditKeyDown(event: KeyboardEvent): void {
277
- event.stopPropagation();
278
- if (event.key === 'Enter') {
279
- this.editCommit.emit();
280
- } else if (event.key === 'Escape') {
281
- this.editCancel.emit();
282
- }
283
- }
284
- }
@@ -1,93 +0,0 @@
1
- export const GRID_BODY_TEMPLATE = `<div
2
- #scrollContainer
3
- style="height: 100%; overflow: auto; position: relative;"
4
- (scroll)="onScroll()">
5
- <div
6
- style="position: relative; min-width: 100%"
7
- [style.width.px]="innerWidth()"
8
- [style.height.px]="sizerHeight()">
9
- <div
10
- class="gp-grid-rows-wrapper"
11
- [style.width.px]="innerWidth()"
12
- [style.transform]="wrapperTransform()">
13
- @for (slot of slotsArray(); track slot.slotId) {
14
- @if (slot.rowIndex >= 0) {
15
- <div
16
- [class]="rowClass(slot.rowIndex, slot.rowData)"
17
- style="position: absolute; top: 0; left: 0"
18
- [style.transform]="'translateY(' + slot.translateY + 'px)'"
19
- [style.width.px]="innerWidth()"
20
- [style.height.px]="rowHeight()"
21
- >
22
- @for (entry of visibleColumnWithIndices(); track entry.originalIndex; let i = $index) {
23
- @let editing = isEditing(slot.rowIndex, entry.originalIndex);
24
- <div
25
- [class]="cellClass(slot.rowIndex, entry.originalIndex, entry.column, slot.rowData)"
26
- style="position: absolute; top: 0;"
27
- [attr.data-row-index]="slot.rowIndex"
28
- [attr.data-col-index]="entry.originalIndex"
29
- [style.left.px]="columnPositions()[i]"
30
- [style.width.px]="columnWidths()[i]"
31
- [style.height.px]="rowHeight()"
32
- (pointerdown)="cellPointerDown.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex, event: $event })"
33
- (mouseenter)="cellPointerEnter.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex })"
34
- (mouseleave)="cellPointerLeave.emit()"
35
- (dblclick)="cellDoubleClick.emit({ rowIndex: slot.rowIndex, colIndex: entry.originalIndex })"
36
- >
37
- @if (editing) {
38
- @let etpl = editTemplate(entry.column);
39
- @if (etpl) {
40
- <ng-container
41
- [ngTemplateOutlet]="etpl"
42
- [ngTemplateOutletContext]="{ $implicit: editParams(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }">
43
- </ng-container>
44
- } @else {
45
- <input
46
- class="gp-grid-edit-input"
47
- type="text"
48
- [value]="editInitialValue()"
49
- autofocus
50
- (focus)="onEditFocus($event)"
51
- (input)="editValueChange.emit(asInput($event).value)"
52
- (keydown)="onEditKeyDown($event)"
53
- (blur)="editCommit.emit()" />
54
- }
55
- } @else {
56
- @let tpl = cellTemplate(entry.column);
57
- @if (tpl) {
58
- <ng-container
59
- [ngTemplateOutlet]="tpl"
60
- [ngTemplateOutletContext]="{ $implicit: cellParams(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }">
61
- </ng-container>
62
- } @else {
63
- {{ cellDisplay(slot.rowData, entry.column, slot.rowIndex, entry.originalIndex) }}
64
- }
65
- }
66
- </div>
67
- }
68
- </div>
69
- }
70
- }
71
- @if (fillHandlePosition(); as fhp) {
72
- @if (editingCell() === null) {
73
- <div
74
- class="gp-grid-fill-handle"
75
- [style.top.px]="fhp.top"
76
- [style.left.px]="fhp.left"
77
- (pointerdown)="fillHandlePointerDown.emit({ event: $event })">
78
- </div>
79
- }
80
- }
81
- @if (rowDropIndicator(); as rd) {
82
- <div
83
- class="gp-grid-row-drop-indicator"
84
- [style.transform]="'translateY(' + rd.dropIndicatorY + 'px)'"
85
- [style.width.px]="rowDropIndicatorWidth()"></div>
86
- }
87
- </div>
88
- </div>
89
- @if (totalRows() === 0) {
90
- <div class="gp-grid-empty">No data to display</div>
91
- }
92
- </div>
93
- `;
@@ -1,206 +0,0 @@
1
- import {
2
- Component,
3
- ChangeDetectionStrategy,
4
- input,
5
- output,
6
- computed,
7
- TemplateRef,
8
- } from "@angular/core";
9
- import { NgTemplateOutlet } from "@angular/common";
10
- import type {
11
- HeaderData,
12
- VisibleColumnInfo,
13
- ColumnDefinition,
14
- HeaderRendererParams,
15
- SortDirection,
16
- } from '@gp-grid/core';
17
-
18
- export type HeaderRendererTemplate = TemplateRef<{ $implicit: HeaderRendererParams }>;
19
-
20
- export interface HeaderSortEvent {
21
- colId: string;
22
- direction: SortDirection | null;
23
- addToExisting: boolean;
24
- }
25
-
26
- export interface HeaderPointerDownEvent {
27
- colIndex: number;
28
- colWidth: number;
29
- colHeight: number;
30
- event: PointerEvent;
31
- }
32
-
33
- export interface FilterPointerDownEvent {
34
- colIndex: number;
35
- anchorEl: HTMLElement;
36
- }
37
-
38
- export interface ResizePointerDownEvent {
39
- colIndex: number;
40
- colWidth: number;
41
- event: PointerEvent;
42
- }
43
-
44
- const TEMPLATE = `
45
- <div
46
- class="gp-grid-header"
47
- [class.gp-grid-header--loading]="isLoading()"
48
- [style.height.px]="headerHeight()">
49
- <div
50
- style="position: absolute; top: 0; left: 0;"
51
- [style.transform]="transformStyle()"
52
- [style.width.px]="innerWidth()"
53
- [style.height.px]="headerHeight()">
54
- @for (entry of visibleColumnsWithIndices(); track entry.originalIndex; let i = $index) {
55
- @let colW = columnWidths()[i] ?? 0;
56
- @let headerData = headers().get(entry.originalIndex);
57
- @let tpl = headerTemplate(entry.column);
58
- <div
59
- class="gp-grid-header-cell"
60
- [attr.data-col-index]="entry.originalIndex"
61
- [style.left.px]="columnPositions()[i]"
62
- [style.width.px]="colW"
63
- [style.height.px]="headerHeight()"
64
- (pointerdown)="onHeaderPointerDown($event, entry.originalIndex, colW)">
65
- @if (tpl) {
66
- <ng-container
67
- [ngTemplateOutlet]="tpl"
68
- [ngTemplateOutletContext]="{ $implicit: headerParams(entry.column, entry.originalIndex, headerData) }">
69
- </ng-container>
70
- } @else {
71
- <span class="gp-grid-header-text">{{ entry.column.headerName ?? entry.column.field }}</span>
72
- <span class="gp-grid-header-icons">
73
- @if (sortingEnabled() && entry.column.sortable !== false) {
74
- <span class="gp-grid-sort-arrows">
75
- <span class="gp-grid-sort-arrows-stack">
76
- <svg
77
- [class]="'gp-grid-sort-arrow-up' + (headerData?.sortDirection === 'asc' ? ' active' : '')"
78
- width="8" height="6" viewBox="0 0 8 6">
79
- <path d="M4 0L8 6H0L4 0Z" fill="currentColor"/>
80
- </svg>
81
- <svg
82
- [class]="'gp-grid-sort-arrow-down' + (headerData?.sortDirection === 'desc' ? ' active' : '')"
83
- width="8" height="6" viewBox="0 0 8 6">
84
- <path d="M4 6L0 0H8L4 6Z" fill="currentColor"/>
85
- </svg>
86
- </span>
87
- @if ((headerData?.sortIndex ?? 0) > 0) {
88
- <span class="gp-grid-sort-index">{{ headerData?.sortIndex }}</span>
89
- }
90
- </span>
91
- }
92
- @if (entry.column.filterable) {
93
- <span
94
- [class]="'gp-grid-filter-icon' + (headerData?.hasFilter ? ' active' : '')"
95
- (pointerdown)="onFilterPointerDown($event, entry.originalIndex)">
96
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
97
- <path d="M4 4h16l-6 8v5l-4 2v-7L4 4z"/>
98
- </svg>
99
- </span>
100
- }
101
- </span>
102
- }
103
- @if (entry.column.resizable !== false) {
104
- <div
105
- class="gp-grid-header-resize-handle"
106
- (pointerdown)="onResizePointerDown($event, entry.originalIndex, colW)">
107
- </div>
108
- }
109
- </div>
110
- }
111
- </div>
112
- </div>
113
- `;
114
-
115
- @Component({
116
- selector: 'gp-grid-header',
117
- standalone: true,
118
- imports: [NgTemplateOutlet],
119
- changeDetection: ChangeDetectionStrategy.OnPush,
120
- template: TEMPLATE,
121
- })
122
- export class GridHeaderComponent {
123
- headerHeight = input.required<number>();
124
- scrollLeft = input.required<number>();
125
- contentWidth = input.required<number>();
126
- totalWidth = input.required<number>();
127
- isLoading = input.required<boolean>();
128
- visibleColumnsWithIndices = input.required<VisibleColumnInfo[]>();
129
- columnPositions = input.required<number[]>();
130
- columnWidths = input.required<number[]>();
131
- headers = input.required<Map<number, HeaderData>>();
132
- sortingEnabled = input<boolean>(true);
133
- headerRenderers = input<Record<string, HeaderRendererTemplate>>({});
134
- globalHeaderRenderer = input<HeaderRendererTemplate | null>(null);
135
-
136
- headerPointerDown = output<HeaderPointerDownEvent>();
137
- filterPointerDown = output<FilterPointerDownEvent>();
138
- resizePointerDown = output<ResizePointerDownEvent>();
139
- headerSort = output<HeaderSortEvent>();
140
- headerFilterOpen = output<{ colIndex: number; anchorEl: HTMLElement }>();
141
-
142
- protected innerWidth = computed(() =>
143
- Math.max(this.contentWidth(), this.totalWidth())
144
- );
145
-
146
- protected transformStyle = computed(() =>
147
- `translateX(${-this.scrollLeft()}px)`
148
- );
149
-
150
- protected onHeaderPointerDown(event: PointerEvent, colIndex: number, colWidth: number): void {
151
- this.headerPointerDown.emit({ colIndex, colWidth, colHeight: this.headerHeight(), event });
152
- }
153
-
154
- protected onFilterPointerDown(event: PointerEvent, colIndex: number): void {
155
- event.stopPropagation();
156
- const cell = (event.currentTarget as HTMLElement).closest('.gp-grid-header-cell') as HTMLElement | null;
157
- if (cell) {
158
- this.filterPointerDown.emit({ colIndex, anchorEl: cell });
159
- }
160
- }
161
-
162
- protected onResizePointerDown(event: PointerEvent, colIndex: number, colWidth: number): void {
163
- event.stopPropagation();
164
- this.resizePointerDown.emit({ colIndex, colWidth, event });
165
- }
166
-
167
- protected headerTemplate(column: ColumnDefinition): HeaderRendererTemplate | null {
168
- const renderer: unknown = column.headerRenderer;
169
- if (renderer instanceof TemplateRef) {
170
- return renderer as HeaderRendererTemplate;
171
- }
172
- if (typeof renderer === 'string') {
173
- const registered = this.headerRenderers()[renderer];
174
- if (registered) return registered;
175
- }
176
- return this.globalHeaderRenderer();
177
- }
178
-
179
- protected headerParams(
180
- column: ColumnDefinition,
181
- colIndex: number,
182
- headerData: HeaderData | undefined,
183
- ): HeaderRendererParams {
184
- const sortable = this.sortingEnabled() && column.sortable !== false;
185
- const filterable = column.filterable === true;
186
- return {
187
- column,
188
- colIndex,
189
- sortDirection: headerData?.sortDirection,
190
- sortIndex: headerData?.sortIndex,
191
- sortable,
192
- filterable,
193
- hasFilter: headerData?.hasFilter ?? false,
194
- onSort: (direction, addToExisting) => {
195
- if (sortable) {
196
- const colId = column.colId ?? column.field;
197
- this.headerSort.emit({ colId, direction, addToExisting });
198
- }
199
- },
200
- onFilterClick: () => {
201
- // The anchor is looked up via data-col-index — same pattern as the default filter icon.
202
- // No-op here; consumers using a custom header template should use the exposed callback.
203
- },
204
- };
205
- }
206
- }