@alaarab/ogrid-angular 2.0.14 → 2.0.16
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-datagrid-table.component.js +71 -1
- package/dist/esm/components/base-inline-cell-editor.component.js +253 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/services/datagrid-state.service.js +33 -102
- package/dist/types/components/base-datagrid-table.component.d.ts +29 -0
- package/dist/types/components/base-inline-cell-editor.component.d.ts +47 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/services/datagrid-state.service.d.ts +1 -3
- package/package.json +2 -2
|
@@ -20,6 +20,10 @@ export class BaseDataGridTableComponent {
|
|
|
20
20
|
this.virtualScrollService = new VirtualScrollService();
|
|
21
21
|
this.lastMouseShift = false;
|
|
22
22
|
this.columnSizingVersion = signal(0);
|
|
23
|
+
/** DOM-measured column widths from the last layout pass.
|
|
24
|
+
* Used as a minWidth floor to prevent columns from shrinking
|
|
25
|
+
* when new data loads (e.g. server-side pagination). */
|
|
26
|
+
this.measuredColumnWidths = signal({});
|
|
23
27
|
// Signal-backed view child elements — set from ngAfterViewInit.
|
|
24
28
|
// @ViewChild is a plain property (not a signal), so effects/computed that read it
|
|
25
29
|
// only evaluate once during construction when the ref is still undefined.
|
|
@@ -129,17 +133,25 @@ export class BaseDataGridTableComponent {
|
|
|
129
133
|
const fc = this.freezeCols();
|
|
130
134
|
const props = this.getProps();
|
|
131
135
|
const pinnedCols = props?.pinnedColumns ?? {};
|
|
136
|
+
const measuredWidths = this.measuredColumnWidths();
|
|
137
|
+
const sizingOverrides = this.columnSizingOverrides();
|
|
132
138
|
return cols.map((col, colIdx) => {
|
|
133
139
|
const isFreezeCol = fc != null && fc >= 1 && colIdx < fc;
|
|
134
140
|
const runtimePinned = pinnedCols[col.columnId];
|
|
135
141
|
const pinnedLeft = runtimePinned === 'left' || col.pinned === 'left' || (isFreezeCol && colIdx === 0);
|
|
136
142
|
const pinnedRight = runtimePinned === 'right' || col.pinned === 'right';
|
|
137
143
|
const w = this.getColumnWidth(col);
|
|
144
|
+
// Use previously-measured DOM width as a minWidth floor to prevent columns
|
|
145
|
+
// from shrinking when new data loads (e.g. server-side pagination).
|
|
146
|
+
const hasResizeOverride = !!sizingOverrides[col.columnId];
|
|
147
|
+
const measuredW = measuredWidths[col.columnId];
|
|
148
|
+
const baseMinWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
149
|
+
const effectiveMinWidth = hasResizeOverride ? w : Math.max(baseMinWidth, measuredW ?? 0);
|
|
138
150
|
return {
|
|
139
151
|
col,
|
|
140
152
|
pinnedLeft,
|
|
141
153
|
pinnedRight,
|
|
142
|
-
minWidth:
|
|
154
|
+
minWidth: effectiveMinWidth,
|
|
143
155
|
width: w,
|
|
144
156
|
};
|
|
145
157
|
});
|
|
@@ -181,6 +193,41 @@ export class BaseDataGridTableComponent {
|
|
|
181
193
|
this.wrapperElSignal.set(wrapper);
|
|
182
194
|
if (tableContainer)
|
|
183
195
|
this.tableContainerElSignal.set(tableContainer);
|
|
196
|
+
this.measureColumnWidths();
|
|
197
|
+
}
|
|
198
|
+
/** Lifecycle hook — re-measure column widths after each view update */
|
|
199
|
+
ngAfterViewChecked() {
|
|
200
|
+
this.measureColumnWidths();
|
|
201
|
+
}
|
|
202
|
+
/** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
|
|
203
|
+
* Only updates the signal when values actually change, to avoid render loops. */
|
|
204
|
+
measureColumnWidths() {
|
|
205
|
+
const wrapper = this.getWrapperRef()?.nativeElement;
|
|
206
|
+
if (!wrapper)
|
|
207
|
+
return;
|
|
208
|
+
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
209
|
+
if (headerCells.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
const measured = {};
|
|
212
|
+
headerCells.forEach((cell) => {
|
|
213
|
+
const colId = cell.getAttribute('data-column-id');
|
|
214
|
+
if (colId)
|
|
215
|
+
measured[colId] = cell.offsetWidth;
|
|
216
|
+
});
|
|
217
|
+
// Only update signal if values changed to avoid triggering computed re-evaluations unnecessarily
|
|
218
|
+
const prev = this.measuredColumnWidths();
|
|
219
|
+
let changed = Object.keys(measured).length !== Object.keys(prev).length;
|
|
220
|
+
if (!changed) {
|
|
221
|
+
for (const key in measured) {
|
|
222
|
+
if (prev[key] !== measured[key]) {
|
|
223
|
+
changed = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (changed) {
|
|
229
|
+
this.measuredColumnWidths.set(measured);
|
|
230
|
+
}
|
|
184
231
|
}
|
|
185
232
|
/**
|
|
186
233
|
* Initialize base wiring effects. Must be called from subclass constructor
|
|
@@ -236,6 +283,25 @@ export class BaseDataGridTableComponent {
|
|
|
236
283
|
});
|
|
237
284
|
}
|
|
238
285
|
// --- Helper methods ---
|
|
286
|
+
/** Lookup effective min-width for a column (includes measured width floor) */
|
|
287
|
+
getEffectiveMinWidth(col) {
|
|
288
|
+
const layout = this.columnLayouts().find((l) => l.col.columnId === col.columnId);
|
|
289
|
+
return layout?.minWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Returns derived cell interaction metadata (non-event attributes) for use in templates.
|
|
293
|
+
* Mirrors React's getCellInteractionProps for the Angular view layer.
|
|
294
|
+
* Event handlers (mousedown, click, dblclick, contextmenu) are still bound inline in templates.
|
|
295
|
+
*/
|
|
296
|
+
getCellInteractionProps(descriptor) {
|
|
297
|
+
return {
|
|
298
|
+
tabIndex: descriptor.isActive ? 0 : -1,
|
|
299
|
+
dataRowIndex: descriptor.rowIndex,
|
|
300
|
+
dataColIndex: descriptor.globalColIndex,
|
|
301
|
+
dataInRange: descriptor.isInRange ? 'true' : null,
|
|
302
|
+
role: descriptor.canEditAny ? 'button' : null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
239
305
|
asColumnDef(colDef) {
|
|
240
306
|
return colDef;
|
|
241
307
|
}
|
|
@@ -332,6 +398,10 @@ export class BaseDataGridTableComponent {
|
|
|
332
398
|
}
|
|
333
399
|
onResizeStart(event, col) {
|
|
334
400
|
event.preventDefault();
|
|
401
|
+
// Clear cell selection before resize (like React) so selection outlines don't persist during drag
|
|
402
|
+
this.state().interaction.setActiveCell(null);
|
|
403
|
+
this.state().interaction.setSelectionRange(null);
|
|
404
|
+
this.getWrapperRef()?.nativeElement.focus({ preventScroll: true });
|
|
335
405
|
const startX = event.clientX;
|
|
336
406
|
const startWidth = this.getColumnWidth(col);
|
|
337
407
|
const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
@@ -0,0 +1,253 @@
|
|
|
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 { Input, Output, EventEmitter, signal, computed, ViewChild } from '@angular/core';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for Angular inline cell editors.
|
|
10
|
+
* Contains all shared signals, lifecycle hooks, keyboard handlers,
|
|
11
|
+
* dropdown positioning, and display logic.
|
|
12
|
+
*
|
|
13
|
+
* Subclasses only need a @Component decorator with selector + template.
|
|
14
|
+
*/
|
|
15
|
+
export class BaseInlineCellEditorComponent {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.commit = new EventEmitter();
|
|
18
|
+
this.cancel = new EventEmitter();
|
|
19
|
+
this.localValue = signal('');
|
|
20
|
+
this.highlightedIndex = signal(0);
|
|
21
|
+
this.selectOptions = signal([]);
|
|
22
|
+
this.searchText = signal('');
|
|
23
|
+
this.filteredOptions = computed(() => {
|
|
24
|
+
const options = this.selectOptions();
|
|
25
|
+
const search = this.searchText().trim().toLowerCase();
|
|
26
|
+
if (!search)
|
|
27
|
+
return options;
|
|
28
|
+
return options.filter((v) => this.getDisplayText(v).toLowerCase().includes(search));
|
|
29
|
+
});
|
|
30
|
+
this._initialized = false;
|
|
31
|
+
}
|
|
32
|
+
ngOnInit() {
|
|
33
|
+
this._initialized = true;
|
|
34
|
+
this.syncFromInputs();
|
|
35
|
+
}
|
|
36
|
+
ngOnChanges() {
|
|
37
|
+
if (this._initialized) {
|
|
38
|
+
this.syncFromInputs();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
syncFromInputs() {
|
|
42
|
+
const v = this.value;
|
|
43
|
+
this.localValue.set(v != null ? String(v) : '');
|
|
44
|
+
const col = this.column;
|
|
45
|
+
if (col?.cellEditorParams?.values) {
|
|
46
|
+
const vals = col.cellEditorParams.values;
|
|
47
|
+
this.selectOptions.set(vals);
|
|
48
|
+
const initialIdx = vals.findIndex((opt) => String(opt) === String(v));
|
|
49
|
+
this.highlightedIndex.set(Math.max(initialIdx, 0));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
ngAfterViewInit() {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
const richSelectInput = this.richSelectInput?.nativeElement;
|
|
55
|
+
if (richSelectInput) {
|
|
56
|
+
richSelectInput.focus();
|
|
57
|
+
richSelectInput.select();
|
|
58
|
+
this.positionFixedDropdown(this.richSelectWrapper, this.richSelectDropdown);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const selectWrap = this.selectWrapper?.nativeElement;
|
|
62
|
+
if (selectWrap) {
|
|
63
|
+
selectWrap.focus();
|
|
64
|
+
this.positionFixedDropdown(this.selectWrapper, this.selectDropdown);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const el = this.inputEl?.nativeElement;
|
|
68
|
+
if (el) {
|
|
69
|
+
el.focus();
|
|
70
|
+
if (el instanceof HTMLInputElement && el.type === 'text') {
|
|
71
|
+
el.select();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
commitValue(value) {
|
|
77
|
+
this.commit.emit(value);
|
|
78
|
+
}
|
|
79
|
+
onTextKeyDown(e) {
|
|
80
|
+
if (e.key === 'Enter') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
this.commitValue(this.localValue());
|
|
83
|
+
}
|
|
84
|
+
else if (e.key === 'Escape') {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
this.cancel.emit();
|
|
87
|
+
}
|
|
88
|
+
else if (e.key === 'Tab') {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
this.commitValue(this.localValue());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getDisplayText(value) {
|
|
94
|
+
const formatValue = this.column?.cellEditorParams?.formatValue;
|
|
95
|
+
if (formatValue)
|
|
96
|
+
return formatValue(value);
|
|
97
|
+
return value != null ? String(value) : '';
|
|
98
|
+
}
|
|
99
|
+
onCustomSelectKeyDown(e) {
|
|
100
|
+
const options = this.selectOptions();
|
|
101
|
+
switch (e.key) {
|
|
102
|
+
case 'ArrowDown':
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
this.highlightedIndex.set(Math.min(this.highlightedIndex() + 1, options.length - 1));
|
|
105
|
+
this.scrollOptionIntoView(this.selectDropdown);
|
|
106
|
+
break;
|
|
107
|
+
case 'ArrowUp':
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
this.highlightedIndex.set(Math.max(this.highlightedIndex() - 1, 0));
|
|
110
|
+
this.scrollOptionIntoView(this.selectDropdown);
|
|
111
|
+
break;
|
|
112
|
+
case 'Enter':
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
if (options.length > 0 && this.highlightedIndex() < options.length) {
|
|
116
|
+
this.commitValue(options[this.highlightedIndex()]);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case 'Tab':
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
if (options.length > 0 && this.highlightedIndex() < options.length) {
|
|
122
|
+
this.commitValue(options[this.highlightedIndex()]);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case 'Escape':
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
this.cancel.emit();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
onRichSelectSearch(text) {
|
|
133
|
+
this.searchText.set(text);
|
|
134
|
+
this.highlightedIndex.set(0);
|
|
135
|
+
}
|
|
136
|
+
onRichSelectKeyDown(e) {
|
|
137
|
+
const options = this.filteredOptions();
|
|
138
|
+
switch (e.key) {
|
|
139
|
+
case 'ArrowDown':
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
this.highlightedIndex.set(Math.min(this.highlightedIndex() + 1, options.length - 1));
|
|
142
|
+
this.scrollOptionIntoView(this.richSelectDropdown);
|
|
143
|
+
break;
|
|
144
|
+
case 'ArrowUp':
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
this.highlightedIndex.set(Math.max(this.highlightedIndex() - 1, 0));
|
|
147
|
+
this.scrollOptionIntoView(this.richSelectDropdown);
|
|
148
|
+
break;
|
|
149
|
+
case 'Enter':
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
e.stopPropagation();
|
|
152
|
+
if (options.length > 0 && this.highlightedIndex() < options.length) {
|
|
153
|
+
this.commitValue(options[this.highlightedIndex()]);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'Escape':
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
this.cancel.emit();
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
onCheckboxKeyDown(e) {
|
|
164
|
+
if (e.key === 'Escape') {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
this.cancel.emit();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
onTextBlur() {
|
|
170
|
+
this.commitValue(this.localValue());
|
|
171
|
+
}
|
|
172
|
+
getInputStyle() {
|
|
173
|
+
const baseStyle = 'width:100%;box-sizing:border-box;padding:6px 10px;border:none;outline:none;font:inherit;background:transparent;color:inherit;';
|
|
174
|
+
const col = this.column;
|
|
175
|
+
if (col.type === 'numeric') {
|
|
176
|
+
return baseStyle + 'text-align:right;';
|
|
177
|
+
}
|
|
178
|
+
return baseStyle;
|
|
179
|
+
}
|
|
180
|
+
/** Position a dropdown using fixed positioning to escape overflow clipping. */
|
|
181
|
+
positionFixedDropdown(wrapperRef, dropdownRef) {
|
|
182
|
+
const wrapper = wrapperRef?.nativeElement;
|
|
183
|
+
const dropdown = dropdownRef?.nativeElement;
|
|
184
|
+
if (!wrapper || !dropdown)
|
|
185
|
+
return;
|
|
186
|
+
const rect = wrapper.getBoundingClientRect();
|
|
187
|
+
const maxH = 200;
|
|
188
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
189
|
+
const flipUp = spaceBelow < maxH && rect.top > spaceBelow;
|
|
190
|
+
dropdown.style.position = 'fixed';
|
|
191
|
+
dropdown.style.left = `${rect.left}px`;
|
|
192
|
+
dropdown.style.width = `${rect.width}px`;
|
|
193
|
+
dropdown.style.maxHeight = `${maxH}px`;
|
|
194
|
+
dropdown.style.zIndex = '9999';
|
|
195
|
+
dropdown.style.right = 'auto';
|
|
196
|
+
if (flipUp) {
|
|
197
|
+
dropdown.style.top = 'auto';
|
|
198
|
+
dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
dropdown.style.top = `${rect.bottom}px`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Scroll the highlighted option into view within a dropdown element. */
|
|
205
|
+
scrollOptionIntoView(dropdownRef) {
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
const dropdown = dropdownRef?.nativeElement;
|
|
208
|
+
if (!dropdown)
|
|
209
|
+
return;
|
|
210
|
+
const highlighted = dropdown.children[this.highlightedIndex()];
|
|
211
|
+
highlighted?.scrollIntoView({ block: 'nearest' });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
__decorate([
|
|
216
|
+
Input({ required: true })
|
|
217
|
+
], BaseInlineCellEditorComponent.prototype, "value", void 0);
|
|
218
|
+
__decorate([
|
|
219
|
+
Input({ required: true })
|
|
220
|
+
], BaseInlineCellEditorComponent.prototype, "item", void 0);
|
|
221
|
+
__decorate([
|
|
222
|
+
Input({ required: true })
|
|
223
|
+
], BaseInlineCellEditorComponent.prototype, "column", void 0);
|
|
224
|
+
__decorate([
|
|
225
|
+
Input({ required: true })
|
|
226
|
+
], BaseInlineCellEditorComponent.prototype, "rowIndex", void 0);
|
|
227
|
+
__decorate([
|
|
228
|
+
Input({ required: true })
|
|
229
|
+
], BaseInlineCellEditorComponent.prototype, "editorType", void 0);
|
|
230
|
+
__decorate([
|
|
231
|
+
Output()
|
|
232
|
+
], BaseInlineCellEditorComponent.prototype, "commit", void 0);
|
|
233
|
+
__decorate([
|
|
234
|
+
Output()
|
|
235
|
+
], BaseInlineCellEditorComponent.prototype, "cancel", void 0);
|
|
236
|
+
__decorate([
|
|
237
|
+
ViewChild('inputEl')
|
|
238
|
+
], BaseInlineCellEditorComponent.prototype, "inputEl", void 0);
|
|
239
|
+
__decorate([
|
|
240
|
+
ViewChild('selectWrapper')
|
|
241
|
+
], BaseInlineCellEditorComponent.prototype, "selectWrapper", void 0);
|
|
242
|
+
__decorate([
|
|
243
|
+
ViewChild('selectDropdown')
|
|
244
|
+
], BaseInlineCellEditorComponent.prototype, "selectDropdown", void 0);
|
|
245
|
+
__decorate([
|
|
246
|
+
ViewChild('richSelectWrapper')
|
|
247
|
+
], BaseInlineCellEditorComponent.prototype, "richSelectWrapper", void 0);
|
|
248
|
+
__decorate([
|
|
249
|
+
ViewChild('richSelectInput')
|
|
250
|
+
], BaseInlineCellEditorComponent.prototype, "richSelectInput", void 0);
|
|
251
|
+
__decorate([
|
|
252
|
+
ViewChild('richSelectDropdown')
|
|
253
|
+
], BaseInlineCellEditorComponent.prototype, "richSelectDropdown", void 0);
|
package/dist/esm/index.js
CHANGED
|
@@ -19,4 +19,5 @@ export { BaseDataGridTableComponent } from './components/base-datagrid-table.com
|
|
|
19
19
|
export { BaseColumnHeaderFilterComponent } from './components/base-column-header-filter.component';
|
|
20
20
|
export { BaseColumnChooserComponent } from './components/base-column-chooser.component';
|
|
21
21
|
export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
|
|
22
|
+
export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
|
|
22
23
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { Injectable, signal, computed, effect, DestroyRef, inject } from '@angular/core';
|
|
8
|
-
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, } from '@alaarab/ogrid-core';
|
|
8
|
+
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, UndoRedoStack, findCtrlArrowTarget, computeTabNavigation, formatSelectionAsTsv, parseTsvClipboard, rangesEqual, } from '@alaarab/ogrid-core';
|
|
9
9
|
/**
|
|
10
10
|
* Single orchestration service for DataGridTable. Takes grid props,
|
|
11
11
|
* returns all derived state and handlers so Angular UI packages can be thin view layers.
|
|
@@ -33,10 +33,8 @@ let DataGridStateService = class DataGridStateService {
|
|
|
33
33
|
this.cutRangeSig = signal(null);
|
|
34
34
|
this.copyRangeSig = signal(null);
|
|
35
35
|
this.internalClipboard = null;
|
|
36
|
-
// Undo/redo state
|
|
37
|
-
this.
|
|
38
|
-
this.redoStack = [];
|
|
39
|
-
this.batch = null;
|
|
36
|
+
// Undo/redo state (backed by core UndoRedoStack)
|
|
37
|
+
this.undoRedoStack = new UndoRedoStack(100);
|
|
40
38
|
this.undoLengthSig = signal(0);
|
|
41
39
|
this.redoLengthSig = signal(0);
|
|
42
40
|
// Fill handle state
|
|
@@ -70,14 +68,10 @@ let DataGridStateService = class DataGridStateService {
|
|
|
70
68
|
if (!original)
|
|
71
69
|
return undefined;
|
|
72
70
|
return (event) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.undoHistory = [...this.undoHistory, [event]].slice(-100);
|
|
78
|
-
this.redoStack = [];
|
|
79
|
-
this.undoLengthSig.set(this.undoHistory.length);
|
|
80
|
-
this.redoLengthSig.set(0);
|
|
71
|
+
this.undoRedoStack.record(event);
|
|
72
|
+
if (!this.undoRedoStack.isBatching) {
|
|
73
|
+
this.undoLengthSig.set(this.undoRedoStack.historyLength);
|
|
74
|
+
this.redoLengthSig.set(this.undoRedoStack.redoLength);
|
|
81
75
|
}
|
|
82
76
|
original(event);
|
|
83
77
|
};
|
|
@@ -355,11 +349,7 @@ let DataGridStateService = class DataGridStateService {
|
|
|
355
349
|
}
|
|
356
350
|
setSelectionRange(range) {
|
|
357
351
|
const prev = this.selectionRangeSig();
|
|
358
|
-
if (prev
|
|
359
|
-
return;
|
|
360
|
-
if (prev && range &&
|
|
361
|
-
prev.startRow === range.startRow && prev.endRow === range.endRow &&
|
|
362
|
-
prev.startCol === range.startCol && prev.endCol === range.endCol)
|
|
352
|
+
if (rangesEqual(prev, range))
|
|
363
353
|
return;
|
|
364
354
|
this.selectionRangeSig.set(range);
|
|
365
355
|
}
|
|
@@ -459,22 +449,7 @@ let DataGridStateService = class DataGridStateService {
|
|
|
459
449
|
if (range == null)
|
|
460
450
|
return;
|
|
461
451
|
const norm = normalizeSelectionRange(range);
|
|
462
|
-
const
|
|
463
|
-
const rows = [];
|
|
464
|
-
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
465
|
-
const cells = [];
|
|
466
|
-
for (let c = norm.startCol; c <= norm.endCol; c++) {
|
|
467
|
-
if (r >= p.items.length || c >= visibleCols.length)
|
|
468
|
-
break;
|
|
469
|
-
const item = p.items[r];
|
|
470
|
-
const col = visibleCols[c];
|
|
471
|
-
const raw = getCellValue(item, col);
|
|
472
|
-
const val = col.valueFormatter ? col.valueFormatter(raw, item) : raw;
|
|
473
|
-
cells.push(val != null && val !== '' ? String(val).replace(/\t/g, ' ').replace(/\n/g, ' ') : '');
|
|
474
|
-
}
|
|
475
|
-
rows.push(cells.join('\t'));
|
|
476
|
-
}
|
|
477
|
-
const tsv = rows.join('\r\n');
|
|
452
|
+
const tsv = formatSelectionAsTsv(p.items, this.visibleCols(), norm);
|
|
478
453
|
this.internalClipboard = tsv;
|
|
479
454
|
this.copyRangeSig.set(norm);
|
|
480
455
|
void navigator.clipboard.writeText(tsv).catch(() => { });
|
|
@@ -515,10 +490,10 @@ let DataGridStateService = class DataGridStateService {
|
|
|
515
490
|
const anchorRow = norm ? norm.startRow : 0;
|
|
516
491
|
const anchorCol = norm ? norm.startCol : 0;
|
|
517
492
|
const visibleCols = this.visibleCols();
|
|
518
|
-
const
|
|
493
|
+
const parsedRows = parseTsvClipboard(text);
|
|
519
494
|
this.beginBatch();
|
|
520
|
-
for (let r = 0; r <
|
|
521
|
-
const cells =
|
|
495
|
+
for (let r = 0; r < parsedRows.length; r++) {
|
|
496
|
+
const cells = parsedRows[r];
|
|
522
497
|
for (let c = 0; c < cells.length; c++) {
|
|
523
498
|
const targetRow = anchorRow + r;
|
|
524
499
|
const targetCol = anchorCol + c;
|
|
@@ -566,28 +541,23 @@ let DataGridStateService = class DataGridStateService {
|
|
|
566
541
|
}
|
|
567
542
|
// --- Undo/Redo ---
|
|
568
543
|
beginBatch() {
|
|
569
|
-
this.
|
|
544
|
+
this.undoRedoStack.beginBatch();
|
|
570
545
|
}
|
|
571
546
|
endBatch() {
|
|
572
|
-
|
|
573
|
-
this.
|
|
574
|
-
|
|
575
|
-
return;
|
|
576
|
-
this.undoHistory = [...this.undoHistory, batch].slice(-100);
|
|
577
|
-
this.redoStack = [];
|
|
578
|
-
this.undoLengthSig.set(this.undoHistory.length);
|
|
579
|
-
this.redoLengthSig.set(0);
|
|
547
|
+
this.undoRedoStack.endBatch();
|
|
548
|
+
this.undoLengthSig.set(this.undoRedoStack.historyLength);
|
|
549
|
+
this.redoLengthSig.set(this.undoRedoStack.redoLength);
|
|
580
550
|
}
|
|
581
551
|
undo() {
|
|
582
552
|
const p = this.props();
|
|
583
553
|
const original = p?.onCellValueChanged;
|
|
584
|
-
if (!original
|
|
554
|
+
if (!original)
|
|
555
|
+
return;
|
|
556
|
+
const lastBatch = this.undoRedoStack.undo();
|
|
557
|
+
if (!lastBatch)
|
|
585
558
|
return;
|
|
586
|
-
|
|
587
|
-
this.
|
|
588
|
-
this.redoStack = [...this.redoStack, lastBatch];
|
|
589
|
-
this.undoLengthSig.set(this.undoHistory.length);
|
|
590
|
-
this.redoLengthSig.set(this.redoStack.length);
|
|
559
|
+
this.undoLengthSig.set(this.undoRedoStack.historyLength);
|
|
560
|
+
this.redoLengthSig.set(this.undoRedoStack.redoLength);
|
|
591
561
|
for (let i = lastBatch.length - 1; i >= 0; i--) {
|
|
592
562
|
const ev = lastBatch[i];
|
|
593
563
|
original({ ...ev, oldValue: ev.newValue, newValue: ev.oldValue });
|
|
@@ -596,13 +566,13 @@ let DataGridStateService = class DataGridStateService {
|
|
|
596
566
|
redo() {
|
|
597
567
|
const p = this.props();
|
|
598
568
|
const original = p?.onCellValueChanged;
|
|
599
|
-
if (!original
|
|
569
|
+
if (!original)
|
|
600
570
|
return;
|
|
601
|
-
const nextBatch = this.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.
|
|
605
|
-
this.
|
|
571
|
+
const nextBatch = this.undoRedoStack.redo();
|
|
572
|
+
if (!nextBatch)
|
|
573
|
+
return;
|
|
574
|
+
this.undoLengthSig.set(this.undoRedoStack.historyLength);
|
|
575
|
+
this.redoLengthSig.set(this.undoRedoStack.redoLength);
|
|
606
576
|
for (const ev of nextBatch) {
|
|
607
577
|
original(ev);
|
|
608
578
|
}
|
|
@@ -645,27 +615,7 @@ let DataGridStateService = class DataGridStateService {
|
|
|
645
615
|
const v = getCellValue(items[r], visibleCols[c]);
|
|
646
616
|
return v == null || v === '';
|
|
647
617
|
};
|
|
648
|
-
const findCtrlTarget =
|
|
649
|
-
if (pos === edge)
|
|
650
|
-
return pos;
|
|
651
|
-
const next = pos + step;
|
|
652
|
-
if (!isEmpty(pos) && !isEmpty(next)) {
|
|
653
|
-
let p = next;
|
|
654
|
-
while (p !== edge) {
|
|
655
|
-
if (isEmpty(p + step))
|
|
656
|
-
return p;
|
|
657
|
-
p += step;
|
|
658
|
-
}
|
|
659
|
-
return edge;
|
|
660
|
-
}
|
|
661
|
-
let pp = next;
|
|
662
|
-
while (pp !== edge) {
|
|
663
|
-
if (!isEmpty(pp))
|
|
664
|
-
return pp;
|
|
665
|
-
pp += step;
|
|
666
|
-
}
|
|
667
|
-
return edge;
|
|
668
|
-
};
|
|
618
|
+
const findCtrlTarget = findCtrlArrowTarget;
|
|
669
619
|
switch (e.key) {
|
|
670
620
|
case 'c':
|
|
671
621
|
if (ctrl) {
|
|
@@ -779,29 +729,10 @@ let DataGridStateService = class DataGridStateService {
|
|
|
779
729
|
}
|
|
780
730
|
case 'Tab': {
|
|
781
731
|
e.preventDefault();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
newColTab = columnIndex - 1;
|
|
787
|
-
}
|
|
788
|
-
else if (rowIndex > 0) {
|
|
789
|
-
newRowTab = rowIndex - 1;
|
|
790
|
-
newColTab = maxColIndex;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
if (columnIndex < maxColIndex) {
|
|
795
|
-
newColTab = columnIndex + 1;
|
|
796
|
-
}
|
|
797
|
-
else if (rowIndex < maxRowIndex) {
|
|
798
|
-
newRowTab = rowIndex + 1;
|
|
799
|
-
newColTab = colOffset;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
const newDataColTab = newColTab - colOffset;
|
|
803
|
-
this.setSelectionRange({ startRow: newRowTab, startCol: newDataColTab, endRow: newRowTab, endCol: newDataColTab });
|
|
804
|
-
this.setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
|
|
732
|
+
const tabResult = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
|
|
733
|
+
const newDataColTab = tabResult.columnIndex - colOffset;
|
|
734
|
+
this.setSelectionRange({ startRow: tabResult.rowIndex, startCol: newDataColTab, endRow: tabResult.rowIndex, endCol: newDataColTab });
|
|
735
|
+
this.setActiveCell({ rowIndex: tabResult.rowIndex, columnIndex: tabResult.columnIndex });
|
|
805
736
|
break;
|
|
806
737
|
}
|
|
807
738
|
case 'Home': {
|
|
@@ -19,6 +19,10 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
19
19
|
readonly virtualScrollService: VirtualScrollService;
|
|
20
20
|
protected lastMouseShift: boolean;
|
|
21
21
|
readonly columnSizingVersion: import("@angular/core").WritableSignal<number>;
|
|
22
|
+
/** DOM-measured column widths from the last layout pass.
|
|
23
|
+
* Used as a minWidth floor to prevent columns from shrinking
|
|
24
|
+
* when new data loads (e.g. server-side pagination). */
|
|
25
|
+
readonly measuredColumnWidths: import("@angular/core").WritableSignal<Record<string, number>>;
|
|
22
26
|
protected readonly wrapperElSignal: import("@angular/core").WritableSignal<HTMLElement | null>;
|
|
23
27
|
protected readonly tableContainerElSignal: import("@angular/core").WritableSignal<HTMLElement | null>;
|
|
24
28
|
/** Return the IOGridDataGridProps from however the subclass receives them */
|
|
@@ -29,6 +33,11 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
29
33
|
protected abstract getTableContainerRef(): ElementRef<HTMLElement> | undefined;
|
|
30
34
|
/** Lifecycle hook — populate element signals from @ViewChild refs */
|
|
31
35
|
ngAfterViewInit(): void;
|
|
36
|
+
/** Lifecycle hook — re-measure column widths after each view update */
|
|
37
|
+
ngAfterViewChecked(): void;
|
|
38
|
+
/** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
|
|
39
|
+
* Only updates the signal when values actually change, to avoid render loops. */
|
|
40
|
+
private measureColumnWidths;
|
|
32
41
|
readonly state: import("@angular/core").Signal<import("../services/datagrid-state.service").DataGridStateResult<T>>;
|
|
33
42
|
readonly tableContainerEl: import("@angular/core").Signal<HTMLElement | null>;
|
|
34
43
|
readonly allItems: import("@angular/core").Signal<T[]>;
|
|
@@ -138,6 +147,26 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
|
|
|
138
147
|
* (effects need to run inside an injection context).
|
|
139
148
|
*/
|
|
140
149
|
protected initBase(): void;
|
|
150
|
+
/** Lookup effective min-width for a column (includes measured width floor) */
|
|
151
|
+
getEffectiveMinWidth(col: IColumnDef<T>): number;
|
|
152
|
+
/**
|
|
153
|
+
* Returns derived cell interaction metadata (non-event attributes) for use in templates.
|
|
154
|
+
* Mirrors React's getCellInteractionProps for the Angular view layer.
|
|
155
|
+
* Event handlers (mousedown, click, dblclick, contextmenu) are still bound inline in templates.
|
|
156
|
+
*/
|
|
157
|
+
getCellInteractionProps(descriptor: {
|
|
158
|
+
isActive: boolean;
|
|
159
|
+
isInRange: boolean;
|
|
160
|
+
canEditAny: boolean;
|
|
161
|
+
globalColIndex: number;
|
|
162
|
+
rowIndex: number;
|
|
163
|
+
}): {
|
|
164
|
+
tabIndex: number;
|
|
165
|
+
dataRowIndex: number;
|
|
166
|
+
dataColIndex: number;
|
|
167
|
+
dataInRange: string | null;
|
|
168
|
+
role: string | null;
|
|
169
|
+
};
|
|
141
170
|
asColumnDef(colDef: unknown): IColumnDef<T>;
|
|
142
171
|
visibleColIndex(col: IColumnDef<T>): number;
|
|
143
172
|
getColumnWidth(col: IColumnDef<T>): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EventEmitter, ElementRef } from '@angular/core';
|
|
2
|
+
import type { IColumnDef } from '@alaarab/ogrid-core';
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for Angular inline cell editors.
|
|
5
|
+
* Contains all shared signals, lifecycle hooks, keyboard handlers,
|
|
6
|
+
* dropdown positioning, and display logic.
|
|
7
|
+
*
|
|
8
|
+
* Subclasses only need a @Component decorator with selector + template.
|
|
9
|
+
*/
|
|
10
|
+
export declare abstract class BaseInlineCellEditorComponent<T = unknown> {
|
|
11
|
+
value: unknown;
|
|
12
|
+
item: T;
|
|
13
|
+
column: IColumnDef<T>;
|
|
14
|
+
rowIndex: number;
|
|
15
|
+
editorType: 'text' | 'select' | 'checkbox' | 'date' | 'richSelect';
|
|
16
|
+
commit: EventEmitter<unknown>;
|
|
17
|
+
cancel: EventEmitter<void>;
|
|
18
|
+
inputEl?: ElementRef<HTMLInputElement | HTMLSelectElement>;
|
|
19
|
+
selectWrapper?: ElementRef<HTMLDivElement>;
|
|
20
|
+
selectDropdown?: ElementRef<HTMLDivElement>;
|
|
21
|
+
richSelectWrapper?: ElementRef<HTMLDivElement>;
|
|
22
|
+
richSelectInput?: ElementRef<HTMLInputElement>;
|
|
23
|
+
richSelectDropdown?: ElementRef<HTMLDivElement>;
|
|
24
|
+
readonly localValue: import("@angular/core").WritableSignal<unknown>;
|
|
25
|
+
readonly highlightedIndex: import("@angular/core").WritableSignal<number>;
|
|
26
|
+
readonly selectOptions: import("@angular/core").WritableSignal<unknown[]>;
|
|
27
|
+
readonly searchText: import("@angular/core").WritableSignal<string>;
|
|
28
|
+
readonly filteredOptions: import("@angular/core").Signal<unknown[]>;
|
|
29
|
+
private _initialized;
|
|
30
|
+
ngOnInit(): void;
|
|
31
|
+
ngOnChanges(): void;
|
|
32
|
+
private syncFromInputs;
|
|
33
|
+
ngAfterViewInit(): void;
|
|
34
|
+
commitValue(value: unknown): void;
|
|
35
|
+
onTextKeyDown(e: KeyboardEvent): void;
|
|
36
|
+
getDisplayText(value: unknown): string;
|
|
37
|
+
onCustomSelectKeyDown(e: KeyboardEvent): void;
|
|
38
|
+
onRichSelectSearch(text: string): void;
|
|
39
|
+
onRichSelectKeyDown(e: KeyboardEvent): void;
|
|
40
|
+
onCheckboxKeyDown(e: KeyboardEvent): void;
|
|
41
|
+
onTextBlur(): void;
|
|
42
|
+
getInputStyle(): string;
|
|
43
|
+
/** Position a dropdown using fixed positioning to escape overflow clipping. */
|
|
44
|
+
protected positionFixedDropdown(wrapperRef: ElementRef<HTMLDivElement> | undefined, dropdownRef: ElementRef<HTMLDivElement> | undefined): void;
|
|
45
|
+
/** Scroll the highlighted option into view within a dropdown element. */
|
|
46
|
+
protected scrollOptionIntoView(dropdownRef: ElementRef<HTMLDivElement> | undefined): void;
|
|
47
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -23,5 +23,6 @@ export type { IColumnHeaderFilterProps } from './components/base-column-header-f
|
|
|
23
23
|
export { BaseColumnChooserComponent } from './components/base-column-chooser.component';
|
|
24
24
|
export type { IColumnChooserProps } from './components/base-column-chooser.component';
|
|
25
25
|
export { BasePaginationControlsComponent } from './components/base-pagination-controls.component';
|
|
26
|
+
export { BaseInlineCellEditorComponent } from './components/base-inline-cell-editor.component';
|
|
26
27
|
export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
|
|
27
28
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -167,9 +167,7 @@ export declare class DataGridStateService<T> {
|
|
|
167
167
|
private readonly cutRangeSig;
|
|
168
168
|
private readonly copyRangeSig;
|
|
169
169
|
private internalClipboard;
|
|
170
|
-
private
|
|
171
|
-
private redoStack;
|
|
172
|
-
private batch;
|
|
170
|
+
private readonly undoRedoStack;
|
|
173
171
|
private readonly undoLengthSig;
|
|
174
172
|
private readonly redoLengthSig;
|
|
175
173
|
private fillDragStart;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-angular",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
38
|
+
"@alaarab/ogrid-core": "2.0.15"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@angular/core": "^21.0.0",
|