@alaarab/ogrid-angular 2.0.8 → 2.0.11
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/dist/esm/components/base-column-chooser.component.js +78 -0
- package/dist/esm/components/base-column-header-filter.component.js +266 -0
- package/dist/esm/components/base-datagrid-table.component.js +116 -5
- package/dist/esm/components/base-pagination-controls.component.js +72 -0
- package/dist/esm/components/empty-state.component.js +22 -10
- package/dist/esm/components/grid-context-menu.component.js +65 -26
- package/dist/esm/components/marching-ants-overlay.component.js +78 -69
- package/dist/esm/components/ogrid-layout.component.js +32 -18
- package/dist/esm/components/sidebar.component.js +45 -42
- package/dist/esm/components/status-bar.component.js +43 -23
- package/dist/esm/index.js +3 -0
- package/dist/esm/services/ogrid.service.js +14 -4
- package/dist/types/components/base-column-chooser.component.d.ts +37 -0
- package/dist/types/components/base-column-header-filter.component.d.ts +90 -0
- package/dist/types/components/base-datagrid-table.component.d.ts +19 -0
- package/dist/types/components/base-pagination-controls.component.d.ts +34 -0
- package/dist/types/components/empty-state.component.d.ts +5 -4
- package/dist/types/components/grid-context-menu.component.d.ts +16 -16
- package/dist/types/components/marching-ants-overlay.component.d.ts +14 -14
- package/dist/types/components/ogrid-layout.component.d.ts +5 -5
- package/dist/types/components/sidebar.component.d.ts +1 -1
- package/dist/types/components/status-bar.component.d.ts +10 -10
- package/dist/types/index.d.ts +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { signal, computed, Input, Output, EventEmitter } from '@angular/core';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class containing all shared TypeScript logic for ColumnChooser components.
|
|
10
|
+
* Framework-specific UI packages extend this with their templates and style overrides.
|
|
11
|
+
*
|
|
12
|
+
* Subclasses must:
|
|
13
|
+
* 1. Provide a @Component decorator with template and styles
|
|
14
|
+
* 2. Handle their own click-outside behavior (host binding or effect)
|
|
15
|
+
*/
|
|
16
|
+
export class BaseColumnChooserComponent {
|
|
17
|
+
constructor() {
|
|
18
|
+
this._columns = signal([]);
|
|
19
|
+
this._visibleColumns = signal(new Set());
|
|
20
|
+
this.visibilityChange = new EventEmitter();
|
|
21
|
+
// Dropdown state
|
|
22
|
+
this.isOpen = signal(false);
|
|
23
|
+
// Computed counts (signal-backed so computed() tracks changes)
|
|
24
|
+
this.visibleCount = computed(() => this._visibleColumns().size);
|
|
25
|
+
this.totalCount = computed(() => this._columns().length);
|
|
26
|
+
}
|
|
27
|
+
set columns(v) { this._columns.set(v); }
|
|
28
|
+
get columns() { return this._columns(); }
|
|
29
|
+
set visibleColumns(v) { this._visibleColumns.set(v); }
|
|
30
|
+
get visibleColumns() { return this._visibleColumns(); }
|
|
31
|
+
toggle() {
|
|
32
|
+
this.isOpen.update((v) => !v);
|
|
33
|
+
}
|
|
34
|
+
onCheckboxChange(columnKey, event) {
|
|
35
|
+
const checked = event.target.checked;
|
|
36
|
+
this.visibilityChange.emit({ columnKey, visible: checked });
|
|
37
|
+
}
|
|
38
|
+
onToggle(columnKey, checked) {
|
|
39
|
+
this.visibilityChange.emit({ columnKey, visible: checked });
|
|
40
|
+
}
|
|
41
|
+
selectAll() {
|
|
42
|
+
for (const col of this.columns) {
|
|
43
|
+
if (!this.visibleColumns.has(col.columnId)) {
|
|
44
|
+
this.visibilityChange.emit({ columnKey: col.columnId, visible: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
clearAll() {
|
|
49
|
+
for (const col of this.columns) {
|
|
50
|
+
if (this.visibleColumns.has(col.columnId)) {
|
|
51
|
+
this.visibilityChange.emit({ columnKey: col.columnId, visible: false });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
onClearAll() {
|
|
56
|
+
for (const col of this.columns) {
|
|
57
|
+
if (col.required !== true && this.visibleColumns.has(col.columnId)) {
|
|
58
|
+
this.visibilityChange.emit({ columnKey: col.columnId, visible: false });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
onSelectAll() {
|
|
63
|
+
for (const col of this.columns) {
|
|
64
|
+
if (!this.visibleColumns.has(col.columnId)) {
|
|
65
|
+
this.visibilityChange.emit({ columnKey: col.columnId, visible: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
__decorate([
|
|
71
|
+
Input({ required: true })
|
|
72
|
+
], BaseColumnChooserComponent.prototype, "columns", null);
|
|
73
|
+
__decorate([
|
|
74
|
+
Input({ required: true })
|
|
75
|
+
], BaseColumnChooserComponent.prototype, "visibleColumns", null);
|
|
76
|
+
__decorate([
|
|
77
|
+
Output()
|
|
78
|
+
], BaseColumnChooserComponent.prototype, "visibilityChange", void 0);
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { signal, computed, Input } from '@angular/core';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class containing all shared TypeScript logic for ColumnHeaderFilter components.
|
|
10
|
+
* Framework-specific UI packages extend this with their templates and style overrides.
|
|
11
|
+
*
|
|
12
|
+
* Subclasses must:
|
|
13
|
+
* 1. Provide a @Component decorator with template and styles
|
|
14
|
+
* 2. Implement abstract accessor for headerEl (ViewChild reference)
|
|
15
|
+
*/
|
|
16
|
+
export class BaseColumnHeaderFilterComponent {
|
|
17
|
+
constructor() {
|
|
18
|
+
// Signal-backed inputs used by computed() — plain @Input properties aren't tracked by computed()
|
|
19
|
+
this._filterType = signal('none');
|
|
20
|
+
this._selectedValues = signal(undefined);
|
|
21
|
+
this._options = signal(undefined);
|
|
22
|
+
this._textValue = signal('');
|
|
23
|
+
this._selectedUser = signal(undefined);
|
|
24
|
+
this._dateValue = signal(undefined);
|
|
25
|
+
// Plain inputs (not used in computed() — no signal wrapper needed)
|
|
26
|
+
this.isSorted = false;
|
|
27
|
+
this.isSortedDescending = false;
|
|
28
|
+
this.onSort = undefined;
|
|
29
|
+
this.onFilterChange = undefined;
|
|
30
|
+
this.isLoadingOptions = false;
|
|
31
|
+
this.onTextChange = undefined;
|
|
32
|
+
this.onUserChange = undefined;
|
|
33
|
+
this.peopleSearch = undefined;
|
|
34
|
+
this.onDateChange = undefined;
|
|
35
|
+
// Internal state signals
|
|
36
|
+
this.isFilterOpen = signal(false);
|
|
37
|
+
this.tempTextValue = signal('');
|
|
38
|
+
this.searchText = signal('');
|
|
39
|
+
this.tempSelected = signal(new Set());
|
|
40
|
+
this.peopleSearchText = signal('');
|
|
41
|
+
this.peopleSuggestions = signal([]);
|
|
42
|
+
this.isPeopleLoading = signal(false);
|
|
43
|
+
this.tempDateFrom = signal('');
|
|
44
|
+
this.tempDateTo = signal('');
|
|
45
|
+
// Popover position
|
|
46
|
+
this.popoverTop = signal(0);
|
|
47
|
+
this.popoverLeft = signal(0);
|
|
48
|
+
this.peopleDebounceTimer = null;
|
|
49
|
+
// Computed signals
|
|
50
|
+
this.hasActiveFilter = computed(() => {
|
|
51
|
+
const ft = this._filterType();
|
|
52
|
+
if (ft === 'text')
|
|
53
|
+
return !!this._textValue();
|
|
54
|
+
if (ft === 'multiSelect')
|
|
55
|
+
return (this._selectedValues()?.length ?? 0) > 0;
|
|
56
|
+
if (ft === 'people')
|
|
57
|
+
return this._selectedUser() != null;
|
|
58
|
+
if (ft === 'date')
|
|
59
|
+
return this._dateValue() != null;
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
this.filteredOptions = computed(() => {
|
|
63
|
+
const opts = this._options() ?? [];
|
|
64
|
+
const search = this.searchText().toLowerCase().trim();
|
|
65
|
+
if (!search)
|
|
66
|
+
return opts;
|
|
67
|
+
return opts.filter((o) => o.toLowerCase().includes(search));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
set filterType(v) { this._filterType.set(v); }
|
|
71
|
+
get filterType() { return this._filterType(); }
|
|
72
|
+
set selectedValues(v) { this._selectedValues.set(v); }
|
|
73
|
+
get selectedValues() { return this._selectedValues(); }
|
|
74
|
+
set options(v) { this._options.set(v); }
|
|
75
|
+
get options() { return this._options(); }
|
|
76
|
+
set textValue(v) { this._textValue.set(v); }
|
|
77
|
+
get textValue() { return this._textValue(); }
|
|
78
|
+
set selectedUser(v) { this._selectedUser.set(v); }
|
|
79
|
+
get selectedUser() { return this._selectedUser(); }
|
|
80
|
+
set dateValue(v) { this._dateValue.set(v); }
|
|
81
|
+
get dateValue() { return this._dateValue(); }
|
|
82
|
+
// Utility methods
|
|
83
|
+
asInputValue(event) {
|
|
84
|
+
return event.target.value;
|
|
85
|
+
}
|
|
86
|
+
toggleFilter(event) {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
if (this.isFilterOpen()) {
|
|
89
|
+
this.isFilterOpen.set(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Initialize temp state from current values
|
|
93
|
+
this.tempTextValue.set(this.textValue);
|
|
94
|
+
this.tempSelected.set(new Set(this.selectedValues ?? []));
|
|
95
|
+
this.searchText.set('');
|
|
96
|
+
this.peopleSearchText.set('');
|
|
97
|
+
this.peopleSuggestions.set([]);
|
|
98
|
+
const dv = this.dateValue;
|
|
99
|
+
this.tempDateFrom.set(dv?.from ?? '');
|
|
100
|
+
this.tempDateTo.set(dv?.to ?? '');
|
|
101
|
+
// Compute popover position
|
|
102
|
+
const el = this.getHeaderEl()?.nativeElement;
|
|
103
|
+
if (el) {
|
|
104
|
+
const rect = el.getBoundingClientRect();
|
|
105
|
+
this.popoverTop.set(rect.bottom + 4);
|
|
106
|
+
this.popoverLeft.set(rect.left);
|
|
107
|
+
}
|
|
108
|
+
this.isFilterOpen.set(true);
|
|
109
|
+
}
|
|
110
|
+
// --- Text filter handlers ---
|
|
111
|
+
onTextKeydown(event) {
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
if (event.key === 'Enter') {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
this.handleTextApply();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
handleTextApply() {
|
|
119
|
+
this.onTextChange(this.tempTextValue());
|
|
120
|
+
this.isFilterOpen.set(false);
|
|
121
|
+
}
|
|
122
|
+
handleTextClear() {
|
|
123
|
+
this.tempTextValue.set('');
|
|
124
|
+
this.onTextChange('');
|
|
125
|
+
this.isFilterOpen.set(false);
|
|
126
|
+
}
|
|
127
|
+
// --- MultiSelect filter handlers ---
|
|
128
|
+
handleCheckboxChange(option, event) {
|
|
129
|
+
const checked = event.target.checked;
|
|
130
|
+
this.tempSelected.update((s) => {
|
|
131
|
+
const next = new Set(s);
|
|
132
|
+
if (checked)
|
|
133
|
+
next.add(option);
|
|
134
|
+
else
|
|
135
|
+
next.delete(option);
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
handleSelectAllFiltered() {
|
|
140
|
+
this.tempSelected.update((s) => {
|
|
141
|
+
const next = new Set(s);
|
|
142
|
+
for (const opt of this.filteredOptions())
|
|
143
|
+
next.add(opt);
|
|
144
|
+
return next;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
handleClearSelection() {
|
|
148
|
+
this.tempSelected.set(new Set());
|
|
149
|
+
}
|
|
150
|
+
handleApplyMultiSelect() {
|
|
151
|
+
this.onFilterChange(Array.from(this.tempSelected()));
|
|
152
|
+
this.isFilterOpen.set(false);
|
|
153
|
+
}
|
|
154
|
+
// --- People filter handlers ---
|
|
155
|
+
onPeopleSearchInput(event) {
|
|
156
|
+
const value = event.target.value;
|
|
157
|
+
this.peopleSearchText.set(value);
|
|
158
|
+
if (this.peopleDebounceTimer)
|
|
159
|
+
clearTimeout(this.peopleDebounceTimer);
|
|
160
|
+
const query = value.trim();
|
|
161
|
+
if (!query) {
|
|
162
|
+
this.peopleSuggestions.set([]);
|
|
163
|
+
this.isPeopleLoading.set(false);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.isPeopleLoading.set(true);
|
|
167
|
+
this.peopleDebounceTimer = setTimeout(() => {
|
|
168
|
+
const fn = this.peopleSearch;
|
|
169
|
+
if (!fn)
|
|
170
|
+
return;
|
|
171
|
+
fn(query)
|
|
172
|
+
.then((results) => {
|
|
173
|
+
this.peopleSuggestions.set(results);
|
|
174
|
+
this.isPeopleLoading.set(false);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => {
|
|
177
|
+
this.peopleSuggestions.set([]);
|
|
178
|
+
this.isPeopleLoading.set(false);
|
|
179
|
+
});
|
|
180
|
+
}, 300);
|
|
181
|
+
}
|
|
182
|
+
handleUserSelect(user) {
|
|
183
|
+
this.onUserChange(user);
|
|
184
|
+
this.isFilterOpen.set(false);
|
|
185
|
+
}
|
|
186
|
+
handleClearUser() {
|
|
187
|
+
this.onUserChange(undefined);
|
|
188
|
+
this.isFilterOpen.set(false);
|
|
189
|
+
}
|
|
190
|
+
// --- Date filter handlers ---
|
|
191
|
+
handleDateApply() {
|
|
192
|
+
const from = this.tempDateFrom();
|
|
193
|
+
const to = this.tempDateTo();
|
|
194
|
+
if (!from && !to) {
|
|
195
|
+
this.onDateChange(undefined);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
this.onDateChange({ from: from || undefined, to: to || undefined });
|
|
199
|
+
}
|
|
200
|
+
this.isFilterOpen.set(false);
|
|
201
|
+
}
|
|
202
|
+
handleDateClear() {
|
|
203
|
+
this.tempDateFrom.set('');
|
|
204
|
+
this.tempDateTo.set('');
|
|
205
|
+
this.onDateChange(undefined);
|
|
206
|
+
this.isFilterOpen.set(false);
|
|
207
|
+
}
|
|
208
|
+
// --- Document click handler (for click-outside to close) ---
|
|
209
|
+
onDocumentClick(event, selectorName) {
|
|
210
|
+
const el = event.target;
|
|
211
|
+
if (!el.closest(selectorName) && !el.closest('.ogrid-header-filter__popover')) {
|
|
212
|
+
this.isFilterOpen.set(false);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
__decorate([
|
|
217
|
+
Input({ required: true })
|
|
218
|
+
], BaseColumnHeaderFilterComponent.prototype, "columnKey", void 0);
|
|
219
|
+
__decorate([
|
|
220
|
+
Input({ required: true })
|
|
221
|
+
], BaseColumnHeaderFilterComponent.prototype, "columnName", void 0);
|
|
222
|
+
__decorate([
|
|
223
|
+
Input({ required: true })
|
|
224
|
+
], BaseColumnHeaderFilterComponent.prototype, "filterType", null);
|
|
225
|
+
__decorate([
|
|
226
|
+
Input()
|
|
227
|
+
], BaseColumnHeaderFilterComponent.prototype, "selectedValues", null);
|
|
228
|
+
__decorate([
|
|
229
|
+
Input()
|
|
230
|
+
], BaseColumnHeaderFilterComponent.prototype, "options", null);
|
|
231
|
+
__decorate([
|
|
232
|
+
Input()
|
|
233
|
+
], BaseColumnHeaderFilterComponent.prototype, "textValue", null);
|
|
234
|
+
__decorate([
|
|
235
|
+
Input()
|
|
236
|
+
], BaseColumnHeaderFilterComponent.prototype, "selectedUser", null);
|
|
237
|
+
__decorate([
|
|
238
|
+
Input()
|
|
239
|
+
], BaseColumnHeaderFilterComponent.prototype, "dateValue", null);
|
|
240
|
+
__decorate([
|
|
241
|
+
Input()
|
|
242
|
+
], BaseColumnHeaderFilterComponent.prototype, "isSorted", void 0);
|
|
243
|
+
__decorate([
|
|
244
|
+
Input()
|
|
245
|
+
], BaseColumnHeaderFilterComponent.prototype, "isSortedDescending", void 0);
|
|
246
|
+
__decorate([
|
|
247
|
+
Input()
|
|
248
|
+
], BaseColumnHeaderFilterComponent.prototype, "onSort", void 0);
|
|
249
|
+
__decorate([
|
|
250
|
+
Input()
|
|
251
|
+
], BaseColumnHeaderFilterComponent.prototype, "onFilterChange", void 0);
|
|
252
|
+
__decorate([
|
|
253
|
+
Input()
|
|
254
|
+
], BaseColumnHeaderFilterComponent.prototype, "isLoadingOptions", void 0);
|
|
255
|
+
__decorate([
|
|
256
|
+
Input()
|
|
257
|
+
], BaseColumnHeaderFilterComponent.prototype, "onTextChange", void 0);
|
|
258
|
+
__decorate([
|
|
259
|
+
Input()
|
|
260
|
+
], BaseColumnHeaderFilterComponent.prototype, "onUserChange", void 0);
|
|
261
|
+
__decorate([
|
|
262
|
+
Input()
|
|
263
|
+
], BaseColumnHeaderFilterComponent.prototype, "peopleSearch", void 0);
|
|
264
|
+
__decorate([
|
|
265
|
+
Input()
|
|
266
|
+
], BaseColumnHeaderFilterComponent.prototype, "onDateChange", void 0);
|
|
@@ -2,7 +2,7 @@ import { signal, computed, effect } from '@angular/core';
|
|
|
2
2
|
import { DataGridStateService } from '../services/datagrid-state.service';
|
|
3
3
|
import { ColumnReorderService } from '../services/column-reorder.service';
|
|
4
4
|
import { VirtualScrollService } from '../services/virtual-scroll.service';
|
|
5
|
-
import { buildHeaderRows, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-core';
|
|
5
|
+
import { buildHeaderRows, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, } from '@alaarab/ogrid-core';
|
|
6
6
|
import { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, } from '../utils';
|
|
7
7
|
/**
|
|
8
8
|
* Abstract base class containing all shared TypeScript logic for DataGridTable components.
|
|
@@ -20,9 +20,14 @@ export class BaseDataGridTableComponent {
|
|
|
20
20
|
this.virtualScrollService = new VirtualScrollService();
|
|
21
21
|
this.lastMouseShift = false;
|
|
22
22
|
this.columnSizingVersion = signal(0);
|
|
23
|
+
// Signal-backed view child elements — set from ngAfterViewInit.
|
|
24
|
+
// @ViewChild is a plain property (not a signal), so effects/computed that read it
|
|
25
|
+
// only evaluate once during construction when the ref is still undefined.
|
|
26
|
+
this.wrapperElSignal = signal(null);
|
|
27
|
+
this.tableContainerElSignal = signal(null);
|
|
23
28
|
// --- Delegated state ---
|
|
24
29
|
this.state = computed(() => this.stateService.getState());
|
|
25
|
-
this.tableContainerEl = computed(() => this.
|
|
30
|
+
this.tableContainerEl = computed(() => this.tableContainerElSignal());
|
|
26
31
|
this.allItems = computed(() => this.getProps()?.items ?? []);
|
|
27
32
|
this.items = computed(() => this.getProps()?.items ?? []);
|
|
28
33
|
this.getRowId = computed(() => this.getProps()?.getRowId ?? ((item) => item['id']));
|
|
@@ -37,6 +42,8 @@ export class BaseDataGridTableComponent {
|
|
|
37
42
|
this.currentPage = computed(() => this.getProps()?.currentPage ?? 1);
|
|
38
43
|
this.pageSize = computed(() => this.getProps()?.pageSize ?? 25);
|
|
39
44
|
this.rowNumberOffset = computed(() => this.hasRowNumbersCol() ? (this.currentPage() - 1) * this.pageSize() : 0);
|
|
45
|
+
this.propsVisibleColumns = computed(() => this.getProps()?.visibleColumns);
|
|
46
|
+
this.propsColumnOrder = computed(() => this.getProps()?.columnOrder);
|
|
40
47
|
// State service outputs
|
|
41
48
|
this.visibleCols = computed(() => this.state().layout.visibleCols);
|
|
42
49
|
this.hasCheckboxCol = computed(() => this.state().layout.hasCheckboxCol);
|
|
@@ -137,6 +144,43 @@ export class BaseDataGridTableComponent {
|
|
|
137
144
|
};
|
|
138
145
|
});
|
|
139
146
|
});
|
|
147
|
+
// Compute sticky offsets for pinned columns (cumulative left/right positions)
|
|
148
|
+
this.pinningOffsets = computed(() => {
|
|
149
|
+
const layouts = this.columnLayouts();
|
|
150
|
+
const leftOffsets = {};
|
|
151
|
+
const rightOffsets = {};
|
|
152
|
+
// Left offsets: start after checkbox and row number columns
|
|
153
|
+
let leftAcc = 0;
|
|
154
|
+
if (this.hasCheckboxCol())
|
|
155
|
+
leftAcc += CHECKBOX_COLUMN_WIDTH;
|
|
156
|
+
if (this.hasRowNumbersCol())
|
|
157
|
+
leftAcc += ROW_NUMBER_COLUMN_WIDTH;
|
|
158
|
+
for (const layout of layouts) {
|
|
159
|
+
if (layout.pinnedLeft) {
|
|
160
|
+
leftOffsets[layout.col.columnId] = leftAcc;
|
|
161
|
+
leftAcc += layout.width + CELL_PADDING;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Right offsets: walk from the end
|
|
165
|
+
let rightAcc = 0;
|
|
166
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
167
|
+
const layout = layouts[i];
|
|
168
|
+
if (layout.pinnedRight) {
|
|
169
|
+
rightOffsets[layout.col.columnId] = rightAcc;
|
|
170
|
+
rightAcc += layout.width + CELL_PADDING;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { leftOffsets, rightOffsets };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Lifecycle hook — populate element signals from @ViewChild refs */
|
|
177
|
+
ngAfterViewInit() {
|
|
178
|
+
const wrapper = this.getWrapperRef()?.nativeElement ?? null;
|
|
179
|
+
const tableContainer = this.getTableContainerRef()?.nativeElement ?? null;
|
|
180
|
+
if (wrapper)
|
|
181
|
+
this.wrapperElSignal.set(wrapper);
|
|
182
|
+
if (tableContainer)
|
|
183
|
+
this.tableContainerElSignal.set(tableContainer);
|
|
140
184
|
}
|
|
141
185
|
/**
|
|
142
186
|
* Initialize base wiring effects. Must be called from subclass constructor
|
|
@@ -149,9 +193,9 @@ export class BaseDataGridTableComponent {
|
|
|
149
193
|
if (p)
|
|
150
194
|
this.stateService.props.set(p);
|
|
151
195
|
});
|
|
152
|
-
// Wire wrapper element
|
|
196
|
+
// Wire wrapper element (reads from signal populated by ngAfterViewInit)
|
|
153
197
|
effect(() => {
|
|
154
|
-
const el = this.
|
|
198
|
+
const el = this.wrapperElSignal();
|
|
155
199
|
if (el) {
|
|
156
200
|
this.stateService.wrapperEl.set(el);
|
|
157
201
|
this.columnReorderService.wrapperEl.set(el);
|
|
@@ -184,7 +228,7 @@ export class BaseDataGridTableComponent {
|
|
|
184
228
|
});
|
|
185
229
|
// Wire wrapper element to virtual scroll for scroll events + container height
|
|
186
230
|
effect(() => {
|
|
187
|
-
const el = this.
|
|
231
|
+
const el = this.wrapperElSignal();
|
|
188
232
|
if (el) {
|
|
189
233
|
this.virtualScrollService.setContainer(el);
|
|
190
234
|
this.virtualScrollService.containerHeight.set(el.clientHeight);
|
|
@@ -239,6 +283,14 @@ export class BaseDataGridTableComponent {
|
|
|
239
283
|
return '';
|
|
240
284
|
return d.toISOString().split('T')[0];
|
|
241
285
|
}
|
|
286
|
+
getPinnedLeftOffset(columnId) {
|
|
287
|
+
const offsets = this.pinningOffsets();
|
|
288
|
+
return offsets.leftOffsets[columnId] ?? null;
|
|
289
|
+
}
|
|
290
|
+
getPinnedRightOffset(columnId) {
|
|
291
|
+
const offsets = this.pinningOffsets();
|
|
292
|
+
return offsets.rightOffsets[columnId] ?? null;
|
|
293
|
+
}
|
|
242
294
|
// --- Virtual scroll event handler ---
|
|
243
295
|
onWrapperScroll(event) {
|
|
244
296
|
this.virtualScrollService.onScroll(event);
|
|
@@ -373,4 +425,63 @@ export class BaseDataGridTableComponent {
|
|
|
373
425
|
canUnpin: !!pinned,
|
|
374
426
|
};
|
|
375
427
|
}
|
|
428
|
+
// --- Column sorting methods ---
|
|
429
|
+
onSortAsc(columnId) {
|
|
430
|
+
const props = this.getProps();
|
|
431
|
+
props?.onColumnSort?.(columnId);
|
|
432
|
+
}
|
|
433
|
+
onSortDesc(columnId) {
|
|
434
|
+
const props = this.getProps();
|
|
435
|
+
props?.onColumnSort?.(columnId);
|
|
436
|
+
}
|
|
437
|
+
onClearSort() {
|
|
438
|
+
// Clearing sort is handled by sorting the same column again
|
|
439
|
+
// The logic in OGridService.handleSort will toggle: asc -> desc -> (clear via callback)
|
|
440
|
+
// For now, we don't have an explicit clear, so we just don't call anything
|
|
441
|
+
}
|
|
442
|
+
getSortState(columnId) {
|
|
443
|
+
const props = this.getProps();
|
|
444
|
+
if (props?.sortBy === columnId) {
|
|
445
|
+
return props.sortDirection ?? 'asc';
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
// --- Column autosize methods ---
|
|
450
|
+
onAutosizeColumn(columnId) {
|
|
451
|
+
const col = this.visibleCols().find((c) => c.columnId === columnId);
|
|
452
|
+
if (!col)
|
|
453
|
+
return;
|
|
454
|
+
const width = this.measureColumnContentWidth(columnId);
|
|
455
|
+
this.state().layout.setColumnSizingOverrides({
|
|
456
|
+
...this.columnSizingOverrides(),
|
|
457
|
+
[columnId]: { widthPx: width },
|
|
458
|
+
});
|
|
459
|
+
this.state().layout.onColumnResized?.(columnId, width);
|
|
460
|
+
}
|
|
461
|
+
onAutosizeAllColumns() {
|
|
462
|
+
const overrides = {};
|
|
463
|
+
for (const col of this.visibleCols()) {
|
|
464
|
+
const width = this.measureColumnContentWidth(col.columnId);
|
|
465
|
+
overrides[col.columnId] = { widthPx: width };
|
|
466
|
+
this.state().layout.onColumnResized?.(col.columnId, width);
|
|
467
|
+
}
|
|
468
|
+
this.state().layout.setColumnSizingOverrides({
|
|
469
|
+
...this.columnSizingOverrides(),
|
|
470
|
+
...overrides,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
measureColumnContentWidth(columnId) {
|
|
474
|
+
const tableEl = this.tableContainerEl();
|
|
475
|
+
if (!tableEl)
|
|
476
|
+
return DEFAULT_MIN_COLUMN_WIDTH;
|
|
477
|
+
const cells = Array.from(tableEl.querySelectorAll(`[data-column-id="${columnId}"]`));
|
|
478
|
+
if (cells.length === 0)
|
|
479
|
+
return DEFAULT_MIN_COLUMN_WIDTH;
|
|
480
|
+
let maxWidth = DEFAULT_MIN_COLUMN_WIDTH;
|
|
481
|
+
for (const cell of cells) {
|
|
482
|
+
const rect = cell.getBoundingClientRect();
|
|
483
|
+
maxWidth = Math.max(maxWidth, Math.ceil(rect.width) + CELL_PADDING);
|
|
484
|
+
}
|
|
485
|
+
return maxWidth;
|
|
486
|
+
}
|
|
376
487
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { signal, computed, Input, Output, EventEmitter } from '@angular/core';
|
|
8
|
+
import { getPaginationViewModel } from '@alaarab/ogrid-core';
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class containing all shared TypeScript logic for PaginationControls components.
|
|
11
|
+
* Framework-specific UI packages extend this with their templates and style overrides.
|
|
12
|
+
*
|
|
13
|
+
* Subclasses must:
|
|
14
|
+
* 1. Provide a @Component decorator with template and styles
|
|
15
|
+
*
|
|
16
|
+
* Uses @Input setter + signal pattern so computed() can track dependencies.
|
|
17
|
+
* (Plain @Input properties are NOT tracked by computed() — only signals are.)
|
|
18
|
+
*/
|
|
19
|
+
export class BasePaginationControlsComponent {
|
|
20
|
+
constructor() {
|
|
21
|
+
this._currentPage = signal(1);
|
|
22
|
+
this._pageSize = signal(25);
|
|
23
|
+
this._totalCount = signal(0);
|
|
24
|
+
this._pageSizeOptions = signal(undefined);
|
|
25
|
+
this._entityLabelPlural = signal('items');
|
|
26
|
+
this.pageChange = new EventEmitter();
|
|
27
|
+
this.pageSizeChange = new EventEmitter();
|
|
28
|
+
this.labelPlural = computed(() => this._entityLabelPlural() ?? 'items');
|
|
29
|
+
this.vm = computed(() => {
|
|
30
|
+
const opts = this._pageSizeOptions();
|
|
31
|
+
return getPaginationViewModel(this._currentPage(), this._pageSize(), this._totalCount(), opts ? { pageSizeOptions: opts } : undefined);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
set currentPage(v) { this._currentPage.set(v); }
|
|
35
|
+
get currentPage() { return this._currentPage(); }
|
|
36
|
+
set pageSize(v) { this._pageSize.set(v); }
|
|
37
|
+
get pageSize() { return this._pageSize(); }
|
|
38
|
+
set totalCount(v) { this._totalCount.set(v); }
|
|
39
|
+
get totalCount() { return this._totalCount(); }
|
|
40
|
+
set pageSizeOptions(v) { this._pageSizeOptions.set(v); }
|
|
41
|
+
get pageSizeOptions() { return this._pageSizeOptions(); }
|
|
42
|
+
set entityLabelPlural(v) { this._entityLabelPlural.set(v); }
|
|
43
|
+
get entityLabelPlural() { return this._entityLabelPlural(); }
|
|
44
|
+
onPageSizeSelect(event) {
|
|
45
|
+
const value = Number(event.target.value);
|
|
46
|
+
this.pageSizeChange.emit(value);
|
|
47
|
+
}
|
|
48
|
+
onPageSizeChange(value) {
|
|
49
|
+
this.pageSizeChange.emit(Number(value));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
__decorate([
|
|
53
|
+
Input({ required: true })
|
|
54
|
+
], BasePaginationControlsComponent.prototype, "currentPage", null);
|
|
55
|
+
__decorate([
|
|
56
|
+
Input({ required: true })
|
|
57
|
+
], BasePaginationControlsComponent.prototype, "pageSize", null);
|
|
58
|
+
__decorate([
|
|
59
|
+
Input({ required: true })
|
|
60
|
+
], BasePaginationControlsComponent.prototype, "totalCount", null);
|
|
61
|
+
__decorate([
|
|
62
|
+
Input()
|
|
63
|
+
], BasePaginationControlsComponent.prototype, "pageSizeOptions", null);
|
|
64
|
+
__decorate([
|
|
65
|
+
Input()
|
|
66
|
+
], BasePaginationControlsComponent.prototype, "entityLabelPlural", null);
|
|
67
|
+
__decorate([
|
|
68
|
+
Output()
|
|
69
|
+
], BasePaginationControlsComponent.prototype, "pageChange", void 0);
|
|
70
|
+
__decorate([
|
|
71
|
+
Output()
|
|
72
|
+
], BasePaginationControlsComponent.prototype, "pageSizeChange", void 0);
|
|
@@ -4,16 +4,28 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { Component,
|
|
7
|
+
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
8
8
|
import { CommonModule } from '@angular/common';
|
|
9
9
|
let EmptyStateComponent = class EmptyStateComponent {
|
|
10
10
|
constructor() {
|
|
11
|
-
this.message =
|
|
12
|
-
this.hasActiveFilters =
|
|
13
|
-
this.render =
|
|
14
|
-
this.clearAll =
|
|
11
|
+
this.message = undefined;
|
|
12
|
+
this.hasActiveFilters = false;
|
|
13
|
+
this.render = undefined;
|
|
14
|
+
this.clearAll = new EventEmitter();
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
+
__decorate([
|
|
18
|
+
Input()
|
|
19
|
+
], EmptyStateComponent.prototype, "message", void 0);
|
|
20
|
+
__decorate([
|
|
21
|
+
Input()
|
|
22
|
+
], EmptyStateComponent.prototype, "hasActiveFilters", void 0);
|
|
23
|
+
__decorate([
|
|
24
|
+
Input()
|
|
25
|
+
], EmptyStateComponent.prototype, "render", void 0);
|
|
26
|
+
__decorate([
|
|
27
|
+
Output()
|
|
28
|
+
], EmptyStateComponent.prototype, "clearAll", void 0);
|
|
17
29
|
EmptyStateComponent = __decorate([
|
|
18
30
|
Component({
|
|
19
31
|
selector: 'ogrid-empty-state',
|
|
@@ -26,11 +38,11 @@ EmptyStateComponent = __decorate([
|
|
|
26
38
|
}
|
|
27
39
|
`],
|
|
28
40
|
template: `
|
|
29
|
-
@if (render
|
|
30
|
-
<ng-container [ngTemplateOutlet]="render
|
|
31
|
-
} @else if (message
|
|
32
|
-
{{ message
|
|
33
|
-
} @else if (hasActiveFilters
|
|
41
|
+
@if (render) {
|
|
42
|
+
<ng-container [ngTemplateOutlet]="render"></ng-container>
|
|
43
|
+
} @else if (message) {
|
|
44
|
+
{{ message }}
|
|
45
|
+
} @else if (hasActiveFilters) {
|
|
34
46
|
No items match your current filters. Try adjusting your search or
|
|
35
47
|
<button type="button" (click)="clearAll.emit()" class="ogrid-empty-state-clear-btn">
|
|
36
48
|
clear all filters
|