ezfw-core 1.0.0
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/components/EzBaseComponent.ts +648 -0
- package/components/EzComponent.ts +89 -0
- package/components/EzInput.module.scss +183 -0
- package/components/EzInput.ts +104 -0
- package/components/EzLabel.ts +22 -0
- package/components/EzOutlet.ts +181 -0
- package/components/HtmlWrapper.ts +305 -0
- package/components/avatar/EzAvatar.module.scss +200 -0
- package/components/avatar/EzAvatar.ts +130 -0
- package/components/badge/EzBadge.module.scss +202 -0
- package/components/badge/EzBadge.ts +77 -0
- package/components/button/EzButton.module.scss +402 -0
- package/components/button/EzButton.ts +175 -0
- package/components/button/EzButtonGroup.ts +48 -0
- package/components/card/EzCard.module.scss +71 -0
- package/components/card/EzCard.ts +120 -0
- package/components/chart/EzBarChart.ts +47 -0
- package/components/chart/EzChart.module.scss +14 -0
- package/components/chart/EzChart.ts +279 -0
- package/components/chart/EzDoughnutChart.ts +47 -0
- package/components/chart/EzLineChart.ts +53 -0
- package/components/checkbox/EzCheckbox.module.scss +145 -0
- package/components/checkbox/EzCheckbox.ts +115 -0
- package/components/dataview/EzDataView.module.scss +115 -0
- package/components/dataview/EzDataView.ts +355 -0
- package/components/dataview/modes/EzDataViewCards.ts +322 -0
- package/components/dataview/modes/EzDataViewGrid.ts +76 -0
- package/components/datepicker/EzDatePicker.module.scss +348 -0
- package/components/datepicker/EzDatePicker.ts +519 -0
- package/components/dialog/EzDialog.module.scss +180 -0
- package/components/dropdown/EzDropdown.module.scss +107 -0
- package/components/dropdown/EzDropdown.ts +235 -0
- package/components/feed/EzActivityFeed.module.scss +90 -0
- package/components/feed/EzActivityFeed.ts +78 -0
- package/components/form/EzForm.ts +364 -0
- package/components/form/EzValidators.test.js +421 -0
- package/components/form/EzValidators.ts +202 -0
- package/components/grid/EzGrid.scss +88 -0
- package/components/grid/EzGrid.ts +1085 -0
- package/components/grid/EzGridContainer.ts +104 -0
- package/components/grid/body/EzGridBody.scss +283 -0
- package/components/grid/body/EzGridBody.ts +549 -0
- package/components/grid/body/EzGridCell.ts +211 -0
- package/components/grid/body/EzGridRow.ts +196 -0
- package/components/grid/filter/EzGridFilters.scss +78 -0
- package/components/grid/filter/EzGridFilters.ts +285 -0
- package/components/grid/footer/EzGridFooter.scss +136 -0
- package/components/grid/footer/EzGridFooter.ts +448 -0
- package/components/grid/header/EzGridHeader.scss +199 -0
- package/components/grid/header/EzGridHeader.ts +430 -0
- package/components/grid/query/EzGridQuery.ts +81 -0
- package/components/grid/state/EzGridColumns.ts +155 -0
- package/components/grid/state/EzGridController.ts +470 -0
- package/components/grid/state/EzGridLifecycle.ts +136 -0
- package/components/grid/state/EzGridNormalizers.test.js +273 -0
- package/components/grid/state/EzGridNormalizers.ts +162 -0
- package/components/grid/state/EzGridParts.ts +233 -0
- package/components/grid/state/EzGridPersistence.ts +140 -0
- package/components/grid/state/EzGridRemote.test.js +573 -0
- package/components/grid/state/EzGridRemote.ts +335 -0
- package/components/grid/state/EzGridSelection.ts +231 -0
- package/components/grid/state/EzGridSort.ts +286 -0
- package/components/grid/title/EzGridActionBar.ts +98 -0
- package/components/grid/title/EzGridTitle.ts +114 -0
- package/components/grid/title/EzGridTitleBar.scss +65 -0
- package/components/grid/title/EzGridTitleBar.ts +87 -0
- package/components/grid/types.ts +607 -0
- package/components/panel/EzPanel.module.scss +133 -0
- package/components/panel/EzPanel.ts +147 -0
- package/components/radio/EzRadio.module.scss +190 -0
- package/components/radio/EzRadio.ts +149 -0
- package/components/select/EzSelect.module.scss +153 -0
- package/components/select/EzSelect.ts +238 -0
- package/components/skeleton/EzSkeleton.module.scss +95 -0
- package/components/skeleton/EzSkeleton.ts +70 -0
- package/components/store/EzStore.ts +344 -0
- package/components/switch/EzSwitch.module.scss +164 -0
- package/components/switch/EzSwitch.ts +117 -0
- package/components/tabs/EzTabPanel.module.scss +181 -0
- package/components/tabs/EzTabPanel.ts +402 -0
- package/components/textarea/EzTextarea.module.scss +131 -0
- package/components/textarea/EzTextarea.ts +161 -0
- package/components/timepicker/EzTimePicker.module.scss +282 -0
- package/components/timepicker/EzTimePicker.ts +540 -0
- package/components/toast/EzToast.module.scss +291 -0
- package/components/tooltip/EzTooltip.module.scss +124 -0
- package/components/tooltip/EzTooltip.ts +153 -0
- package/core/EzComponentTypes.ts +693 -0
- package/core/EzError.ts +63 -0
- package/core/EzModel.ts +268 -0
- package/core/EzTypes.ts +328 -0
- package/core/eventBus.ts +284 -0
- package/core/ez.ts +617 -0
- package/core/loader.ts +725 -0
- package/core/renderer.ts +1010 -0
- package/core/router.ts +490 -0
- package/core/services.ts +124 -0
- package/core/state.ts +142 -0
- package/core/utils.ts +81 -0
- package/package.json +51 -0
- package/services/RouteUI.js +17 -0
- package/services/crypto.js +64 -0
- package/services/dialog.js +222 -0
- package/services/fetchApi.js +63 -0
- package/services/firebase.js +30 -0
- package/services/toast.js +214 -0
- package/template/doc/EzDocs.js +15 -0
- package/template/doc/EzDocs.module.scss +627 -0
- package/template/doc/EzDocsController.js +164 -0
- package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
- package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
- package/template/doc/data/badge/EzBadgeDoc.js +92 -0
- package/template/doc/data/button/EzButtonDoc.js +77 -0
- package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
- package/template/doc/data/card/EzCardDoc.js +39 -0
- package/template/doc/data/chart/EzChartDoc.js +60 -0
- package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
- package/template/doc/data/component/EzComponentDoc.js +34 -0
- package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
- package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
- package/template/doc/data/dialog/EzDialogDoc.js +217 -0
- package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
- package/template/doc/data/form/EzFormDoc.js +90 -0
- package/template/doc/data/grid/EzGridDoc.js +99 -0
- package/template/doc/data/input/EzInputDoc.js +92 -0
- package/template/doc/data/label/EzLabelDoc.js +40 -0
- package/template/doc/data/model/EzModelDoc.js +53 -0
- package/template/doc/data/outlet/EzOutletDoc.js +63 -0
- package/template/doc/data/panel/EzPanelDoc.js +214 -0
- package/template/doc/data/radio/EzRadioDoc.js +174 -0
- package/template/doc/data/router/EzRouterDoc.js +75 -0
- package/template/doc/data/select/EzSelectDoc.js +37 -0
- package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
- package/template/doc/data/switch/EzSwitchDoc.js +82 -0
- package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
- package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
- package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
- package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
- package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
- package/template/doc/sidebar/EzDocsSidebar.js +32 -0
- package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
- package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
- package/template/doc/viewer/EzDocsViewer.js +18 -0
- package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
- package/template/doc/viewer/content/EzDocsContent.js +315 -0
- package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
- package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
- package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
- package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
- package/template/doc/welcome/EzDocsWelcome.js +48 -0
- package/themes/ez-theme.scss +179 -0
- package/themes/nature-fresh.scss +169 -0
- package/types/global.d.ts +21 -0
- package/utils/cssModules.js +81 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// EzGrid/body/EzGridBody.ts
|
|
2
|
+
|
|
3
|
+
import './EzGridBody.scss';
|
|
4
|
+
import { EzGridContainer, EzGridContainerConfig } from '../EzGridContainer.js';
|
|
5
|
+
import type {
|
|
6
|
+
NormalizedColumn,
|
|
7
|
+
EzGridController,
|
|
8
|
+
RowData,
|
|
9
|
+
RowKeyFn,
|
|
10
|
+
RowCallback,
|
|
11
|
+
RowContextMenuCallback,
|
|
12
|
+
RowClickContext
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
import type { EzGridSelection } from '../state/EzGridSelection.js';
|
|
15
|
+
|
|
16
|
+
declare const ez: {
|
|
17
|
+
_createElement(config: unknown): Promise<HTMLElement | null>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface EzGridBodyRef {
|
|
21
|
+
selectAll?: () => void;
|
|
22
|
+
clearSelection?: () => void;
|
|
23
|
+
selectRange?: (toIndex: number) => void;
|
|
24
|
+
toggleRow?: (row: RowData, index: number) => void;
|
|
25
|
+
isSelected?: (row: RowData) => boolean;
|
|
26
|
+
selection?: EzGridSelection;
|
|
27
|
+
controller?: EzGridController;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EzGridBodyConfig extends Omit<EzGridContainerConfig, 'controller'> {
|
|
31
|
+
grid?: EzGridBodyRef;
|
|
32
|
+
controller?: EzGridController;
|
|
33
|
+
columns?: (() => NormalizedColumn[]) | NormalizedColumn[];
|
|
34
|
+
rowKey?: RowKeyFn;
|
|
35
|
+
selection?: EzGridSelection;
|
|
36
|
+
rowHeight?: number;
|
|
37
|
+
virtual?: boolean;
|
|
38
|
+
bufferRows?: number;
|
|
39
|
+
keyboard?: boolean;
|
|
40
|
+
emptyText?: string;
|
|
41
|
+
onRowClick?: RowCallback;
|
|
42
|
+
onRowDoubleClick?: RowCallback;
|
|
43
|
+
onRowContextMenu?: RowContextMenuCallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class EzGridBody extends EzGridContainer {
|
|
47
|
+
declare config: EzGridBodyConfig;
|
|
48
|
+
declare el: HTMLElement | null;
|
|
49
|
+
|
|
50
|
+
data: RowData[];
|
|
51
|
+
private _isLoading: boolean;
|
|
52
|
+
private _error: Error | string | null;
|
|
53
|
+
private _focusedIndex: number;
|
|
54
|
+
private _keyboardEnabled: boolean;
|
|
55
|
+
private _virtual: boolean;
|
|
56
|
+
private _rowHeight: number;
|
|
57
|
+
private _bufferRows: number;
|
|
58
|
+
private _visibleStart: number;
|
|
59
|
+
private _visibleEnd: number;
|
|
60
|
+
private _scrollRAF: number | null;
|
|
61
|
+
private _loadingEl: HTMLElement | null;
|
|
62
|
+
private _errorEl: HTMLElement | null;
|
|
63
|
+
private _emptyEl: HTMLElement | null;
|
|
64
|
+
private _virtualContainer: HTMLElement | null;
|
|
65
|
+
|
|
66
|
+
constructor(config: EzGridBodyConfig = {}) {
|
|
67
|
+
super(config as EzGridContainerConfig);
|
|
68
|
+
|
|
69
|
+
this.config.cls = (this.config.cls || '') + ' ez-grid-body';
|
|
70
|
+
|
|
71
|
+
this.data = [];
|
|
72
|
+
|
|
73
|
+
// State tracking for loading/error UI
|
|
74
|
+
this._isLoading = false;
|
|
75
|
+
this._error = null;
|
|
76
|
+
|
|
77
|
+
// Keyboard navigation state
|
|
78
|
+
this._focusedIndex = -1;
|
|
79
|
+
this._keyboardEnabled = this.config.keyboard !== false;
|
|
80
|
+
|
|
81
|
+
// Virtual scrolling config
|
|
82
|
+
this._virtual = this.config.virtual === true;
|
|
83
|
+
this._rowHeight = this.config.rowHeight ?? 40;
|
|
84
|
+
this._bufferRows = this.config.bufferRows ?? 5;
|
|
85
|
+
this._visibleStart = 0;
|
|
86
|
+
this._visibleEnd = 0;
|
|
87
|
+
this._scrollRAF = null;
|
|
88
|
+
|
|
89
|
+
// UI element refs
|
|
90
|
+
this._loadingEl = null;
|
|
91
|
+
this._errorEl = null;
|
|
92
|
+
this._emptyEl = null;
|
|
93
|
+
this._virtualContainer = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async render(): Promise<HTMLDivElement> {
|
|
97
|
+
const el = document.createElement('div');
|
|
98
|
+
|
|
99
|
+
// layout
|
|
100
|
+
el.style.display = 'flex';
|
|
101
|
+
el.style.flexDirection = 'column';
|
|
102
|
+
el.style.flex = String(this.config.flex ?? '1');
|
|
103
|
+
el.style.minHeight = '0';
|
|
104
|
+
el.style.overflowY = 'auto';
|
|
105
|
+
|
|
106
|
+
if (this.config.cls) {
|
|
107
|
+
el.className = Array.isArray(this.config.cls)
|
|
108
|
+
? this.config.cls.join(' ')
|
|
109
|
+
: String(this.config.cls);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Make focusable for keyboard navigation
|
|
113
|
+
if (this._keyboardEnabled) {
|
|
114
|
+
el.setAttribute('tabindex', '0');
|
|
115
|
+
el.setAttribute('role', 'grid');
|
|
116
|
+
this._bindKeyboardEvents(el);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Virtual scrolling setup
|
|
120
|
+
if (this._virtual) {
|
|
121
|
+
el.classList.add('ez-grid-body--virtual');
|
|
122
|
+
this._bindScrollEvents(el);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.el = el;
|
|
126
|
+
|
|
127
|
+
return el;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private _bindScrollEvents(el: HTMLElement): void {
|
|
131
|
+
el.addEventListener('scroll', () => {
|
|
132
|
+
if (this._scrollRAF) return;
|
|
133
|
+
|
|
134
|
+
this._scrollRAF = requestAnimationFrame(() => {
|
|
135
|
+
this._scrollRAF = null;
|
|
136
|
+
this._onVirtualScroll();
|
|
137
|
+
});
|
|
138
|
+
}, { passive: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _onVirtualScroll(): void {
|
|
142
|
+
if (!this._virtual || !this.data?.length) return;
|
|
143
|
+
|
|
144
|
+
const scrollTop = this.el!.scrollTop;
|
|
145
|
+
const viewportHeight = this.el!.clientHeight;
|
|
146
|
+
|
|
147
|
+
const startIndex = Math.floor(scrollTop / this._rowHeight);
|
|
148
|
+
const visibleCount = Math.ceil(viewportHeight / this._rowHeight);
|
|
149
|
+
|
|
150
|
+
const newStart = Math.max(0, startIndex - this._bufferRows);
|
|
151
|
+
const newEnd = Math.min(
|
|
152
|
+
this.data.length,
|
|
153
|
+
startIndex + visibleCount + this._bufferRows
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Only re-render if visible range changed
|
|
157
|
+
if (newStart !== this._visibleStart || newEnd !== this._visibleEnd) {
|
|
158
|
+
this._visibleStart = newStart;
|
|
159
|
+
this._visibleEnd = newEnd;
|
|
160
|
+
this._renderVirtualRows();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async _renderVirtualRows(): Promise<void> {
|
|
165
|
+
if (!this._virtualContainer) return;
|
|
166
|
+
|
|
167
|
+
// Clear current rows
|
|
168
|
+
this._virtualContainer.innerHTML = '';
|
|
169
|
+
|
|
170
|
+
// Render visible rows
|
|
171
|
+
for (let i = this._visibleStart; i < this._visibleEnd; i++) {
|
|
172
|
+
const row = this.data[i];
|
|
173
|
+
if (!row) continue;
|
|
174
|
+
|
|
175
|
+
const rowCfg = this._createRowConfig(row, i);
|
|
176
|
+
const rowEl = await ez._createElement(rowCfg);
|
|
177
|
+
|
|
178
|
+
if (rowEl) {
|
|
179
|
+
// Position row absolutely
|
|
180
|
+
rowEl.style.position = 'absolute';
|
|
181
|
+
rowEl.style.top = `${i * this._rowHeight}px`;
|
|
182
|
+
rowEl.style.left = '0';
|
|
183
|
+
rowEl.style.right = '0';
|
|
184
|
+
rowEl.style.height = `${this._rowHeight}px`;
|
|
185
|
+
|
|
186
|
+
this._virtualContainer.appendChild(rowEl);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private _bindKeyboardEvents(el: HTMLElement): void {
|
|
192
|
+
el.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
193
|
+
this._handleKeyDown(e);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private _handleKeyDown(e: KeyboardEvent): void {
|
|
198
|
+
if (!this.data?.length) return;
|
|
199
|
+
|
|
200
|
+
const maxIndex = this.data.length - 1;
|
|
201
|
+
|
|
202
|
+
switch (e.key) {
|
|
203
|
+
case 'ArrowDown':
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
this._moveFocus(1);
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case 'ArrowUp':
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
this._moveFocus(-1);
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'Home':
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
this._setFocusedIndex(0);
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'End':
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
this._setFocusedIndex(maxIndex);
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'PageDown':
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
this._moveFocus(10);
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case 'PageUp':
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
this._moveFocus(-10);
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'Enter':
|
|
234
|
+
case ' ':
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
this._selectFocusedRow(e);
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'a':
|
|
240
|
+
case 'A':
|
|
241
|
+
if (e.ctrlKey || e.metaKey) {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
this.config.grid?.selectAll?.();
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'Escape':
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
this.config.grid?.clearSelection?.();
|
|
250
|
+
this._setFocusedIndex(-1);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private _moveFocus(delta: number): void {
|
|
256
|
+
const maxIndex = this.data.length - 1;
|
|
257
|
+
let newIndex = this._focusedIndex + delta;
|
|
258
|
+
|
|
259
|
+
// Clamp to valid range
|
|
260
|
+
newIndex = Math.max(0, Math.min(newIndex, maxIndex));
|
|
261
|
+
|
|
262
|
+
this._setFocusedIndex(newIndex);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private _setFocusedIndex(index: number): void {
|
|
266
|
+
const prevIndex = this._focusedIndex;
|
|
267
|
+
this._focusedIndex = index;
|
|
268
|
+
|
|
269
|
+
// Update visual focus
|
|
270
|
+
if (prevIndex >= 0) {
|
|
271
|
+
const prevRow = this._getRowElement(prevIndex);
|
|
272
|
+
prevRow?.classList.remove('is-focused');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (index >= 0) {
|
|
276
|
+
const row = this._getRowElement(index);
|
|
277
|
+
if (row) {
|
|
278
|
+
row.classList.add('is-focused');
|
|
279
|
+
this._scrollRowIntoView(row);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private _getRowElement(index: number): HTMLElement | null {
|
|
285
|
+
if (!this.el) return null;
|
|
286
|
+
return this.el.querySelector(`.ez-grid-row:nth-child(${index + 1})`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _scrollRowIntoView(row: HTMLElement): void {
|
|
290
|
+
if (!row || !this.el) return;
|
|
291
|
+
|
|
292
|
+
const bodyRect = this.el.getBoundingClientRect();
|
|
293
|
+
const rowRect = row.getBoundingClientRect();
|
|
294
|
+
|
|
295
|
+
if (rowRect.top < bodyRect.top) {
|
|
296
|
+
row.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
297
|
+
} else if (rowRect.bottom > bodyRect.bottom) {
|
|
298
|
+
row.scrollIntoView({ block: 'end', behavior: 'smooth' });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private _selectFocusedRow(e: KeyboardEvent): void {
|
|
303
|
+
if (this._focusedIndex < 0 || !this.data) return;
|
|
304
|
+
|
|
305
|
+
const row = this.data[this._focusedIndex];
|
|
306
|
+
const grid = this.config.grid;
|
|
307
|
+
|
|
308
|
+
if (!grid || !row) return;
|
|
309
|
+
|
|
310
|
+
if (e.shiftKey) {
|
|
311
|
+
grid.selectRange?.(this._focusedIndex);
|
|
312
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
313
|
+
// Toggle selection without clearing others
|
|
314
|
+
grid.toggleRow?.(row, this._focusedIndex);
|
|
315
|
+
} else {
|
|
316
|
+
grid.toggleRow?.(row, this._focusedIndex);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setLoading(loading: boolean): void {
|
|
321
|
+
this._isLoading = !!loading;
|
|
322
|
+
|
|
323
|
+
if (!this.el) return;
|
|
324
|
+
|
|
325
|
+
if (this._isLoading) {
|
|
326
|
+
this._showLoadingOverlay();
|
|
327
|
+
} else {
|
|
328
|
+
this._hideLoadingOverlay();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
setError(error: Error | string | null): void {
|
|
333
|
+
this._error = error;
|
|
334
|
+
|
|
335
|
+
if (!this.el) return;
|
|
336
|
+
|
|
337
|
+
if (this._error) {
|
|
338
|
+
this._showErrorState();
|
|
339
|
+
} else {
|
|
340
|
+
this._hideErrorState();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private _showLoadingOverlay(): void {
|
|
345
|
+
if (this._loadingEl) return;
|
|
346
|
+
|
|
347
|
+
this._loadingEl = document.createElement('div');
|
|
348
|
+
this._loadingEl.className = 'ez-grid-loading';
|
|
349
|
+
this._loadingEl.innerHTML = `
|
|
350
|
+
<div class="ez-grid-loading-spinner"></div>
|
|
351
|
+
<span class="ez-grid-loading-text">Loading...</span>
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
this.el!.appendChild(this._loadingEl);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private _hideLoadingOverlay(): void {
|
|
358
|
+
if (this._loadingEl) {
|
|
359
|
+
this._loadingEl.remove();
|
|
360
|
+
this._loadingEl = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private _showErrorState(): void {
|
|
365
|
+
// Remove existing error if any
|
|
366
|
+
this._hideErrorState();
|
|
367
|
+
|
|
368
|
+
const message = this._error instanceof Error
|
|
369
|
+
? this._error.message
|
|
370
|
+
: String(this._error);
|
|
371
|
+
|
|
372
|
+
this._errorEl = document.createElement('div');
|
|
373
|
+
this._errorEl.className = 'ez-grid-error';
|
|
374
|
+
this._errorEl.innerHTML = `
|
|
375
|
+
<div class="ez-grid-error-icon">⚠</div>
|
|
376
|
+
<div class="ez-grid-error-message">${this._escapeHtml(message)}</div>
|
|
377
|
+
<button class="ez-grid-error-retry">Retry</button>
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
// Retry button handler
|
|
381
|
+
const retryBtn = this._errorEl.querySelector('.ez-grid-error-retry');
|
|
382
|
+
retryBtn?.addEventListener('click', () => {
|
|
383
|
+
(this.config.grid as any)?.controller?.reload?.();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
this.el!.innerHTML = '';
|
|
387
|
+
this.el!.appendChild(this._errorEl);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private _hideErrorState(): void {
|
|
391
|
+
if (this._errorEl) {
|
|
392
|
+
this._errorEl.remove();
|
|
393
|
+
this._errorEl = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private _showEmptyState(): void {
|
|
398
|
+
const emptyText = this.config.emptyText ?? 'No data available';
|
|
399
|
+
|
|
400
|
+
this._emptyEl = document.createElement('div');
|
|
401
|
+
this._emptyEl.className = 'ez-grid-empty';
|
|
402
|
+
this._emptyEl.textContent = emptyText;
|
|
403
|
+
|
|
404
|
+
this.el!.appendChild(this._emptyEl);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private _escapeHtml(str: string): string {
|
|
408
|
+
const div = document.createElement('div');
|
|
409
|
+
div.textContent = str;
|
|
410
|
+
return div.innerHTML;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
updateSelectionVisuals(): void {
|
|
414
|
+
if (!this.el) return;
|
|
415
|
+
|
|
416
|
+
const grid = this.config.grid;
|
|
417
|
+
if (!grid) return;
|
|
418
|
+
|
|
419
|
+
const rows = this.el.querySelectorAll('.ez-grid-row[data-ez-row-id]');
|
|
420
|
+
|
|
421
|
+
rows.forEach(rowEl => {
|
|
422
|
+
const rowId = rowEl.getAttribute('data-ez-row-id');
|
|
423
|
+
const isSelected = grid.selection?.selected?.has?.(String(rowId));
|
|
424
|
+
|
|
425
|
+
if (isSelected) {
|
|
426
|
+
rowEl.classList.add('is-selected');
|
|
427
|
+
} else {
|
|
428
|
+
rowEl.classList.remove('is-selected');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Also update checkbox if exists
|
|
432
|
+
const checkbox = rowEl.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
|
|
433
|
+
if (checkbox) {
|
|
434
|
+
checkbox.checked = !!isSelected;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async refreshData(data: any[]): Promise<void> {
|
|
440
|
+
this.data = Array.isArray(data) ? data : [];
|
|
441
|
+
|
|
442
|
+
// Reset focus when data changes
|
|
443
|
+
this._focusedIndex = -1;
|
|
444
|
+
|
|
445
|
+
// Clear current content
|
|
446
|
+
if (this.el) {
|
|
447
|
+
this.el.innerHTML = '';
|
|
448
|
+
this._loadingEl = null;
|
|
449
|
+
this._errorEl = null;
|
|
450
|
+
this._emptyEl = null;
|
|
451
|
+
this._virtualContainer = null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Show loading if active
|
|
455
|
+
if (this._isLoading) {
|
|
456
|
+
this._showLoadingOverlay();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Show error if present
|
|
461
|
+
if (this._error) {
|
|
462
|
+
this._showErrorState();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Show empty state if no data
|
|
467
|
+
if (this.data.length === 0) {
|
|
468
|
+
this._showEmptyState();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Virtual scrolling mode
|
|
473
|
+
if (this._virtual) {
|
|
474
|
+
await this._renderVirtual();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Standard rendering - all rows
|
|
479
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
480
|
+
const row = this.data[i];
|
|
481
|
+
|
|
482
|
+
const rowCfg = this._createRowConfig(row, i);
|
|
483
|
+
const rowEl = await ez._createElement(rowCfg);
|
|
484
|
+
|
|
485
|
+
if (rowEl) {
|
|
486
|
+
this.el!.appendChild(rowEl);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async _renderVirtual(): Promise<void> {
|
|
492
|
+
// Create spacer for total height
|
|
493
|
+
const totalHeight = this.data.length * this._rowHeight;
|
|
494
|
+
|
|
495
|
+
// Virtual container with absolute positioning
|
|
496
|
+
this._virtualContainer = document.createElement('div');
|
|
497
|
+
this._virtualContainer.className = 'ez-grid-virtual-container';
|
|
498
|
+
this._virtualContainer.style.position = 'relative';
|
|
499
|
+
this._virtualContainer.style.height = `${totalHeight}px`;
|
|
500
|
+
this._virtualContainer.style.width = '100%';
|
|
501
|
+
|
|
502
|
+
this.el!.appendChild(this._virtualContainer);
|
|
503
|
+
|
|
504
|
+
// Calculate initial visible range
|
|
505
|
+
const viewportHeight = this.el!.clientHeight || 400;
|
|
506
|
+
const visibleCount = Math.ceil(viewportHeight / this._rowHeight);
|
|
507
|
+
|
|
508
|
+
this._visibleStart = 0;
|
|
509
|
+
this._visibleEnd = Math.min(this.data.length, visibleCount + this._bufferRows);
|
|
510
|
+
|
|
511
|
+
// Render initial rows
|
|
512
|
+
await this._renderVirtualRows();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private _createRowConfig(row: any, index: number): any {
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
eztype: 'EzGridRow',
|
|
519
|
+
row,
|
|
520
|
+
index,
|
|
521
|
+
rowCount: this.data.length,
|
|
522
|
+
columns: typeof this.config.columns === 'function'
|
|
523
|
+
? this.config.columns()
|
|
524
|
+
: this.config.columns,
|
|
525
|
+
rowKey: this.config.rowKey,
|
|
526
|
+
selection: this.config.selection,
|
|
527
|
+
rowHeight: this.config.rowHeight,
|
|
528
|
+
onRowClick: this.config.onRowClick,
|
|
529
|
+
onRowDoubleClick: this.config.onRowDoubleClick,
|
|
530
|
+
onRowContextMenu: this.config.onRowContextMenu,
|
|
531
|
+
|
|
532
|
+
onClick: (e: MouseEvent) => {
|
|
533
|
+
const grid = this.config.grid;
|
|
534
|
+
|
|
535
|
+
if (!grid) return;
|
|
536
|
+
|
|
537
|
+
if (e.shiftKey) {
|
|
538
|
+
grid.selectRange?.(index);
|
|
539
|
+
} else {
|
|
540
|
+
grid.toggleRow?.(row, index);
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
controller: this.config.controller,
|
|
545
|
+
grid: this.config.grid
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
}
|
|
549
|
+
}
|