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.
Files changed (154) hide show
  1. package/components/EzBaseComponent.ts +648 -0
  2. package/components/EzComponent.ts +89 -0
  3. package/components/EzInput.module.scss +183 -0
  4. package/components/EzInput.ts +104 -0
  5. package/components/EzLabel.ts +22 -0
  6. package/components/EzOutlet.ts +181 -0
  7. package/components/HtmlWrapper.ts +305 -0
  8. package/components/avatar/EzAvatar.module.scss +200 -0
  9. package/components/avatar/EzAvatar.ts +130 -0
  10. package/components/badge/EzBadge.module.scss +202 -0
  11. package/components/badge/EzBadge.ts +77 -0
  12. package/components/button/EzButton.module.scss +402 -0
  13. package/components/button/EzButton.ts +175 -0
  14. package/components/button/EzButtonGroup.ts +48 -0
  15. package/components/card/EzCard.module.scss +71 -0
  16. package/components/card/EzCard.ts +120 -0
  17. package/components/chart/EzBarChart.ts +47 -0
  18. package/components/chart/EzChart.module.scss +14 -0
  19. package/components/chart/EzChart.ts +279 -0
  20. package/components/chart/EzDoughnutChart.ts +47 -0
  21. package/components/chart/EzLineChart.ts +53 -0
  22. package/components/checkbox/EzCheckbox.module.scss +145 -0
  23. package/components/checkbox/EzCheckbox.ts +115 -0
  24. package/components/dataview/EzDataView.module.scss +115 -0
  25. package/components/dataview/EzDataView.ts +355 -0
  26. package/components/dataview/modes/EzDataViewCards.ts +322 -0
  27. package/components/dataview/modes/EzDataViewGrid.ts +76 -0
  28. package/components/datepicker/EzDatePicker.module.scss +348 -0
  29. package/components/datepicker/EzDatePicker.ts +519 -0
  30. package/components/dialog/EzDialog.module.scss +180 -0
  31. package/components/dropdown/EzDropdown.module.scss +107 -0
  32. package/components/dropdown/EzDropdown.ts +235 -0
  33. package/components/feed/EzActivityFeed.module.scss +90 -0
  34. package/components/feed/EzActivityFeed.ts +78 -0
  35. package/components/form/EzForm.ts +364 -0
  36. package/components/form/EzValidators.test.js +421 -0
  37. package/components/form/EzValidators.ts +202 -0
  38. package/components/grid/EzGrid.scss +88 -0
  39. package/components/grid/EzGrid.ts +1085 -0
  40. package/components/grid/EzGridContainer.ts +104 -0
  41. package/components/grid/body/EzGridBody.scss +283 -0
  42. package/components/grid/body/EzGridBody.ts +549 -0
  43. package/components/grid/body/EzGridCell.ts +211 -0
  44. package/components/grid/body/EzGridRow.ts +196 -0
  45. package/components/grid/filter/EzGridFilters.scss +78 -0
  46. package/components/grid/filter/EzGridFilters.ts +285 -0
  47. package/components/grid/footer/EzGridFooter.scss +136 -0
  48. package/components/grid/footer/EzGridFooter.ts +448 -0
  49. package/components/grid/header/EzGridHeader.scss +199 -0
  50. package/components/grid/header/EzGridHeader.ts +430 -0
  51. package/components/grid/query/EzGridQuery.ts +81 -0
  52. package/components/grid/state/EzGridColumns.ts +155 -0
  53. package/components/grid/state/EzGridController.ts +470 -0
  54. package/components/grid/state/EzGridLifecycle.ts +136 -0
  55. package/components/grid/state/EzGridNormalizers.test.js +273 -0
  56. package/components/grid/state/EzGridNormalizers.ts +162 -0
  57. package/components/grid/state/EzGridParts.ts +233 -0
  58. package/components/grid/state/EzGridPersistence.ts +140 -0
  59. package/components/grid/state/EzGridRemote.test.js +573 -0
  60. package/components/grid/state/EzGridRemote.ts +335 -0
  61. package/components/grid/state/EzGridSelection.ts +231 -0
  62. package/components/grid/state/EzGridSort.ts +286 -0
  63. package/components/grid/title/EzGridActionBar.ts +98 -0
  64. package/components/grid/title/EzGridTitle.ts +114 -0
  65. package/components/grid/title/EzGridTitleBar.scss +65 -0
  66. package/components/grid/title/EzGridTitleBar.ts +87 -0
  67. package/components/grid/types.ts +607 -0
  68. package/components/panel/EzPanel.module.scss +133 -0
  69. package/components/panel/EzPanel.ts +147 -0
  70. package/components/radio/EzRadio.module.scss +190 -0
  71. package/components/radio/EzRadio.ts +149 -0
  72. package/components/select/EzSelect.module.scss +153 -0
  73. package/components/select/EzSelect.ts +238 -0
  74. package/components/skeleton/EzSkeleton.module.scss +95 -0
  75. package/components/skeleton/EzSkeleton.ts +70 -0
  76. package/components/store/EzStore.ts +344 -0
  77. package/components/switch/EzSwitch.module.scss +164 -0
  78. package/components/switch/EzSwitch.ts +117 -0
  79. package/components/tabs/EzTabPanel.module.scss +181 -0
  80. package/components/tabs/EzTabPanel.ts +402 -0
  81. package/components/textarea/EzTextarea.module.scss +131 -0
  82. package/components/textarea/EzTextarea.ts +161 -0
  83. package/components/timepicker/EzTimePicker.module.scss +282 -0
  84. package/components/timepicker/EzTimePicker.ts +540 -0
  85. package/components/toast/EzToast.module.scss +291 -0
  86. package/components/tooltip/EzTooltip.module.scss +124 -0
  87. package/components/tooltip/EzTooltip.ts +153 -0
  88. package/core/EzComponentTypes.ts +693 -0
  89. package/core/EzError.ts +63 -0
  90. package/core/EzModel.ts +268 -0
  91. package/core/EzTypes.ts +328 -0
  92. package/core/eventBus.ts +284 -0
  93. package/core/ez.ts +617 -0
  94. package/core/loader.ts +725 -0
  95. package/core/renderer.ts +1010 -0
  96. package/core/router.ts +490 -0
  97. package/core/services.ts +124 -0
  98. package/core/state.ts +142 -0
  99. package/core/utils.ts +81 -0
  100. package/package.json +51 -0
  101. package/services/RouteUI.js +17 -0
  102. package/services/crypto.js +64 -0
  103. package/services/dialog.js +222 -0
  104. package/services/fetchApi.js +63 -0
  105. package/services/firebase.js +30 -0
  106. package/services/toast.js +214 -0
  107. package/template/doc/EzDocs.js +15 -0
  108. package/template/doc/EzDocs.module.scss +627 -0
  109. package/template/doc/EzDocsController.js +164 -0
  110. package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
  111. package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
  112. package/template/doc/data/badge/EzBadgeDoc.js +92 -0
  113. package/template/doc/data/button/EzButtonDoc.js +77 -0
  114. package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
  115. package/template/doc/data/card/EzCardDoc.js +39 -0
  116. package/template/doc/data/chart/EzChartDoc.js +60 -0
  117. package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
  118. package/template/doc/data/component/EzComponentDoc.js +34 -0
  119. package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
  120. package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
  121. package/template/doc/data/dialog/EzDialogDoc.js +217 -0
  122. package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
  123. package/template/doc/data/form/EzFormDoc.js +90 -0
  124. package/template/doc/data/grid/EzGridDoc.js +99 -0
  125. package/template/doc/data/input/EzInputDoc.js +92 -0
  126. package/template/doc/data/label/EzLabelDoc.js +40 -0
  127. package/template/doc/data/model/EzModelDoc.js +53 -0
  128. package/template/doc/data/outlet/EzOutletDoc.js +63 -0
  129. package/template/doc/data/panel/EzPanelDoc.js +214 -0
  130. package/template/doc/data/radio/EzRadioDoc.js +174 -0
  131. package/template/doc/data/router/EzRouterDoc.js +75 -0
  132. package/template/doc/data/select/EzSelectDoc.js +37 -0
  133. package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
  134. package/template/doc/data/switch/EzSwitchDoc.js +82 -0
  135. package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
  136. package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
  137. package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
  138. package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
  139. package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
  140. package/template/doc/sidebar/EzDocsSidebar.js +32 -0
  141. package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
  142. package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
  143. package/template/doc/viewer/EzDocsViewer.js +18 -0
  144. package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
  145. package/template/doc/viewer/content/EzDocsContent.js +315 -0
  146. package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
  147. package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
  148. package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
  149. package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
  150. package/template/doc/welcome/EzDocsWelcome.js +48 -0
  151. package/themes/ez-theme.scss +179 -0
  152. package/themes/nature-fresh.scss +169 -0
  153. package/types/global.d.ts +21 -0
  154. package/utils/cssModules.js +81 -0
@@ -0,0 +1,1085 @@
1
+ import './EzGrid.scss';
2
+ import { EzGridContainer } from './EzGridContainer.js';
3
+ import { EzError } from '../../core/EzError.js';
4
+ import { EzGridSelection } from './state/EzGridSelection.js';
5
+ import { EzGridColumns } from './state/EzGridColumns.js';
6
+ import { EzGridSort } from './state/EzGridSort.js';
7
+ import { EzGridLifecycle } from './state/EzGridLifecycle.js';
8
+ import { createEzGridController } from './state/EzGridController.js';
9
+ import { EzGridPersistence } from './state/EzGridPersistence.js';
10
+ import { EzGridNormalizers } from './state/EzGridNormalizers.js';
11
+ import { EzGridParts } from './state/EzGridParts.js';
12
+ import { EzGridRemote } from './state/EzGridRemote.js';
13
+ import type {
14
+ EzGridConfig,
15
+ EzGridModel,
16
+ EventCallback,
17
+ RowCallback,
18
+ FilterSnapshot,
19
+ ExportCSVOptions,
20
+ ExportExcelOptions,
21
+ RowData,
22
+ NormalizedColumn,
23
+ EzGridController
24
+ } from './types.js';
25
+
26
+ /* eslint-disable @typescript-eslint/no-explicit-any */
27
+
28
+ declare const ez: {
29
+ getController(name: string): any;
30
+ getGridBehavior(name: string): Promise<Record<string, unknown>>;
31
+ getModelSync(name: string): EzGridModel | null;
32
+ _loader: {
33
+ resolveModel(name: string): Promise<void>;
34
+ };
35
+ };
36
+
37
+ // ==========================================================
38
+ // EzGrid Class
39
+ // ==========================================================
40
+
41
+ export class EzGrid extends EzGridContainer {
42
+ declare config: any; // EzGridConfig - relaxed for compatibility
43
+
44
+ // Selection
45
+ _selection: any;
46
+ selection: any;
47
+ _suppressSelectionPersistence: boolean;
48
+
49
+ // Columns
50
+ _columns: any;
51
+ columns: any[];
52
+ columnState: any;
53
+
54
+ // Header
55
+ header: any;
56
+
57
+ // Sort
58
+ _sort: any;
59
+ sort: any;
60
+
61
+ // Lifecycle & Persistence
62
+ _lifecycle: any;
63
+ _persistence: any;
64
+ _parts: any;
65
+
66
+ // Controller
67
+ controller: any;
68
+
69
+ // State flags
70
+ stateful: boolean;
71
+ statefulPersist: boolean;
72
+ rowKey: any;
73
+
74
+ // Local sort/filter baselines
75
+ _localSortBaseline: unknown[] | null;
76
+ _hasLocalSortBaseline: boolean;
77
+ _localFilterBaseline: unknown[] | null;
78
+ _hasLocalFilterBaseline: boolean;
79
+ _lastFilterSnapshot: FilterSnapshot[] | null = null;
80
+
81
+ // Store mode
82
+ _store: any;
83
+ _storeUnsubscribers: Array<() => void>;
84
+
85
+ // Remote mode
86
+ _remote: any = null;
87
+ _autoLoadPending: boolean = false;
88
+ _overRulesApplied: boolean = false;
89
+
90
+ // Callbacks
91
+ _onLoadingChanged: (loading: boolean) => void;
92
+ _onError: (error: Error | null) => void;
93
+ _rowClickHandler: RowCallback | null = null;
94
+ _rowDoubleClickHandler: RowCallback | null = null;
95
+ _rowContextMenuHandler: RowCallback | null = null;
96
+
97
+ // Instances
98
+ el: HTMLElement | null = null;
99
+ headerInstance?: any;
100
+ _bodyInstance?: any;
101
+ _footerInstance?: any;
102
+ _filtersInstance?: any;
103
+
104
+ constructor(config: EzGridConfig = {}) {
105
+ super(config as any);
106
+
107
+ this._selection = new EzGridSelection(this as any, config.selection);
108
+ this.selection = this._selection;
109
+ this._suppressSelectionPersistence = true;
110
+
111
+ this.header = EzGridNormalizers.normalizeHeader(config.header);
112
+
113
+ const selectionConfig = typeof config.selection === 'object' ? config.selection : undefined;
114
+ const normalizedColumns = EzGridNormalizers.normalizeColumns(
115
+ config.columns || [],
116
+ {
117
+ selectionMode: selectionConfig?.mode,
118
+ selectionWidth: selectionConfig?.width,
119
+ headerAlign: this.header?.align
120
+ }
121
+ );
122
+
123
+ this._columns = new EzGridColumns(this, normalizedColumns);
124
+ this.columns = normalizedColumns;
125
+ this.columnState = this._columns.state;
126
+
127
+ this._sort = new EzGridSort(this as any, config.sort);
128
+ this.sort = this._sort;
129
+
130
+ this._lifecycle = new EzGridLifecycle(this as any);
131
+ this._persistence = new EzGridPersistence(this as any);
132
+ this._parts = new EzGridParts(this as any);
133
+
134
+ this._localSortBaseline = null;
135
+ this._hasLocalSortBaseline = false;
136
+ this._localFilterBaseline = null;
137
+ this._hasLocalFilterBaseline = false;
138
+
139
+ const variant = config.variant || 'elevated';
140
+
141
+ this.config.cls = [
142
+ ...(Array.isArray(this.config.cls) ? this.config.cls : [this.config.cls]),
143
+ 'ez-grid',
144
+ `ez-grid--${variant}`
145
+ ].filter(Boolean) as string[];
146
+
147
+ this.config.layout = 'vbox';
148
+ this.config.flex ??= 1;
149
+
150
+ this.stateful = config.stateful || false;
151
+ this.statefulPersist = !!config.statefulPersist;
152
+
153
+ this.rowKey = EzGridNormalizers.normalizeRowKey(config.rowKey);
154
+
155
+ this.config.skipInit = true;
156
+
157
+ this._onLoadingChanged = (loading: boolean) => {
158
+ this._lifecycle.onLoadingChanged(loading);
159
+ };
160
+
161
+ this._onError = (error: Error | null) => {
162
+ this._lifecycle.onError(error);
163
+ };
164
+
165
+ this._initController();
166
+
167
+ this._store = null;
168
+ this._storeUnsubscribers = [];
169
+
170
+ if (config.store) {
171
+ this._initStore(config);
172
+ } else if (config.remote) {
173
+ this._initRemote(config);
174
+ }
175
+
176
+ this.controller.on('datachange', () => {
177
+ Promise.resolve().then(() => {
178
+ void this._onControllerDataChange();
179
+ });
180
+ });
181
+
182
+ this._resolveListeners();
183
+ this._buildParts();
184
+ }
185
+
186
+ // ==========================================================
187
+ // ListenTo Resolution
188
+ // ==========================================================
189
+
190
+ private _resolveListeners(): void {
191
+ const listenTo = this.config.listenTo;
192
+
193
+ this._rowClickHandler = this._resolveRowCallback(
194
+ listenTo?.onRowClick ?? this.config.onRowClick
195
+ );
196
+ this._rowDoubleClickHandler = this._resolveRowCallback(
197
+ listenTo?.onRowDoubleClick ?? this.config.onRowDoubleClick
198
+ );
199
+ this._rowContextMenuHandler = this._resolveRowCallback(
200
+ listenTo?.onRowContextMenu ?? this.config.onRowContextMenu
201
+ );
202
+ }
203
+
204
+ private _resolveRowCallback(callback: RowCallback | string | undefined | null): RowCallback | null {
205
+ if (typeof callback === 'function') {
206
+ return callback;
207
+ }
208
+
209
+ if (typeof callback === 'string') {
210
+ if (callback.includes(':')) {
211
+ const [controllerName, fnName] = callback.split(':');
212
+ const controller = ez.getController(controllerName);
213
+
214
+ if (controller && typeof controller[fnName] === 'function') {
215
+ return (controller[fnName] as RowCallback).bind(controller);
216
+ }
217
+
218
+ console.warn(`[EzGrid] Could not resolve listenTo callback: ${callback}`);
219
+ return null;
220
+ }
221
+
222
+ const controllerName = this.config?.controller;
223
+ if (controllerName) {
224
+ const controller = ez.getController(controllerName);
225
+ if (controller && typeof controller[callback] === 'function') {
226
+ return (controller[callback] as RowCallback).bind(controller);
227
+ }
228
+ }
229
+
230
+ return (...args: unknown[]) => {
231
+ const name = this.config?.controller;
232
+ if (name) {
233
+ const ctrl = ez.getController(name);
234
+ if (ctrl && typeof ctrl[callback] === 'function') {
235
+ return (ctrl[callback] as RowCallback).apply(ctrl, args as any);
236
+ }
237
+ }
238
+ console.warn(`[EzGrid] Could not resolve listenTo callback: ${callback}`);
239
+ };
240
+ }
241
+
242
+ return null;
243
+ }
244
+
245
+ // ==========================================================
246
+ // Public Rendering API
247
+ // ==========================================================
248
+
249
+ async render(): Promise<HTMLDivElement> {
250
+ await this._ensureOverRulesApplied();
251
+ await this._bindData();
252
+
253
+ const originalInit = this.config.init;
254
+ this.config.init = undefined;
255
+
256
+ const el = await super.render();
257
+ this.el = el;
258
+
259
+ this.config.init = originalInit;
260
+
261
+ if (typeof this.config.init === 'function') {
262
+ queueMicrotask(() => {
263
+ this.config.init!(this);
264
+ });
265
+ }
266
+
267
+ Promise.resolve().then(() => {
268
+ void this._onControllerDataChange();
269
+ });
270
+
271
+ if (this._autoLoadPending) {
272
+ this._autoLoadPending = false;
273
+ queueMicrotask(() => {
274
+ this.controller.load();
275
+ });
276
+ }
277
+
278
+ return el;
279
+ }
280
+
281
+ async refreshBody(): Promise<void> {
282
+ if (this.controller?.state?.mode === 'remote') {
283
+ this.controller.reload();
284
+ return;
285
+ }
286
+
287
+ this._lifecycle?.onControllerDataChange?.();
288
+ }
289
+
290
+ // ==========================================================
291
+ // Public Event API
292
+ // ==========================================================
293
+
294
+ on(event: string, fn: EventCallback): void {
295
+ if (!this.controller?.on) return;
296
+ this.controller.on(event, fn);
297
+ }
298
+
299
+ off(event: string, fn: EventCallback): void {
300
+ if (!this.controller?.off) return;
301
+ this.controller.off(event, fn);
302
+ }
303
+
304
+ // ==========================================================
305
+ // Selection Public API
306
+ // ==========================================================
307
+
308
+ _isSelectionEnabled(): boolean {
309
+ return this._selection?.isEnabled?.() === true;
310
+ }
311
+
312
+ isSelected(row: unknown): boolean {
313
+ return this._selection?.isSelected(row) === true;
314
+ }
315
+
316
+ getSelection(): unknown[] {
317
+ return this._selection?.getSelection() ?? [];
318
+ }
319
+
320
+ toggleRow(row: unknown, index: number): void {
321
+ this._selection?.toggleRow(row, index);
322
+ }
323
+
324
+ selectRange(toIndex: number): void {
325
+ this._selection?.selectRange(toIndex);
326
+ }
327
+
328
+ clearSelection(): void {
329
+ this._selection?.clear();
330
+ }
331
+
332
+ selectAll(): void {
333
+ this._selection?.selectAll();
334
+ }
335
+
336
+ isAllSelected(): boolean {
337
+ return this._selection?.isAllSelected() === true;
338
+ }
339
+
340
+ _getSelectionStateSnapshot(): any {
341
+ return this._selection?.snapshot?.() ?? null;
342
+ }
343
+
344
+ _applySelectionStateSnapshot(snapshot: any): void {
345
+ this._selection?.restore?.(snapshot);
346
+ }
347
+
348
+ _onSelectionChanged(): void {
349
+ if (this.stateful && this._suppressSelectionPersistence !== true) {
350
+ this._persistence.updateSelectionState(this._getSelectionStateSnapshot());
351
+ this._persistence.save();
352
+ }
353
+
354
+ this._emitSelectionChange();
355
+ this._bodyInstance?.updateSelectionVisuals?.();
356
+ }
357
+
358
+ private _emitSelectionChange(): void {
359
+ const controller = this.controller;
360
+ if (!controller?.emit) return;
361
+
362
+ const keys = Array.from(this.selection?.selected || []);
363
+ const data = this.controller?.state?.data || [];
364
+ const rows = data.filter((row: any) => {
365
+ const key = this._getRowKey(row);
366
+ return key != null && this.selection.selected.has(String(key));
367
+ });
368
+
369
+ controller.emit('selectionchange', {
370
+ grid: this,
371
+ mode: this.selection?.mode ?? 'single',
372
+ keys,
373
+ rows
374
+ });
375
+ }
376
+
377
+ // ==========================================================
378
+ // Column Public API
379
+ // ==========================================================
380
+
381
+ getVisibleColumns(): any[] {
382
+ return this._columns.getVisibleColumns();
383
+ }
384
+
385
+ hideColumn(colId: string): void {
386
+ this._columns.hide(colId);
387
+ }
388
+
389
+ showColumn(colId: string): void {
390
+ this._columns.show(colId);
391
+ }
392
+
393
+ toggleColumn(colId: string): void {
394
+ this._columns.toggle(colId);
395
+ }
396
+
397
+ setColumnWidth(colId: string, width: number): void {
398
+ this._columns.setWidth(colId, width);
399
+ }
400
+
401
+ resetColumnWidth(colId: string): void {
402
+ this._columns.resetWidth(colId);
403
+ }
404
+
405
+ moveColumn(colId: string, toIndex: number): void {
406
+ this._columns.move(colId, toIndex);
407
+ }
408
+
409
+ _getColumnStateSnapshot(): any {
410
+ return this._columns.snapshot();
411
+ }
412
+
413
+ _onColumnsChanged(): void {
414
+ if (this.stateful) {
415
+ this._persistence.updateColumnState(this._columns.snapshot());
416
+ }
417
+
418
+ this.headerInstance?.refreshColumns?.();
419
+ void this.refreshBody();
420
+ this._persistence.save();
421
+ }
422
+
423
+ // ==========================================================
424
+ // Sort Public API
425
+ // ==========================================================
426
+
427
+ toggleSort(property: string): void {
428
+ this._sort.toggleSort(property);
429
+ }
430
+
431
+ clearSort(): void {
432
+ this._sort.clear();
433
+ }
434
+
435
+ getSorters(): any[] {
436
+ return this._sort.getSorters();
437
+ }
438
+
439
+ _getSortStateSnapshot(): any {
440
+ return this._sort.snapshot();
441
+ }
442
+
443
+ _applySortStateSnapshot(snapshot: any): void {
444
+ this._sort.restore(snapshot);
445
+ }
446
+
447
+ sortBy(colId: string, direction: string): void {
448
+ this._sort.sortBy(colId, direction);
449
+ }
450
+
451
+ isColumnSorted(colId: string): boolean {
452
+ return this._sort.isSorted(colId);
453
+ }
454
+
455
+ getColumnSortDirection(colId: string): string | null {
456
+ const sorter = this._sort.getSorterFor(colId);
457
+ return sorter?.direction ?? null;
458
+ }
459
+
460
+ // ==========================================================
461
+ // Filter Public API
462
+ // ==========================================================
463
+
464
+ _onFiltersChanged(snapshot: FilterSnapshot[] | null): void {
465
+ this.controller?.setFilterSnapshot?.(snapshot);
466
+
467
+ if (this.controller?.state?.mode !== 'remote') {
468
+ this._applyLocalFilters(snapshot);
469
+ }
470
+ }
471
+
472
+ private _applyLocalFilters(snapshot: FilterSnapshot[] | null): void {
473
+ if (!this._hasLocalFilterBaseline) {
474
+ this._localFilterBaseline = this._localSortBaseline
475
+ ? [...this._localSortBaseline]
476
+ : [...(this.controller.state.data ?? [])];
477
+ this._hasLocalFilterBaseline = true;
478
+ }
479
+
480
+ const baseline = this._localFilterBaseline ?? [];
481
+
482
+ if (!snapshot || !Array.isArray(snapshot) || snapshot.length === 0) {
483
+ this._lastFilterSnapshot = null;
484
+ this.controller.state.data = [...baseline];
485
+
486
+ if (this._localSortBaseline) {
487
+ this._localSortBaseline = [...baseline];
488
+ }
489
+
490
+ if (this._sort?.sorters?.length) {
491
+ this._sort.applyLocalSort();
492
+ } else {
493
+ void this.refreshBody();
494
+ }
495
+ return;
496
+ }
497
+
498
+ this._lastFilterSnapshot = snapshot;
499
+
500
+ const filtered = baseline.filter(row => {
501
+ return this._matchesAllFilters(row, snapshot);
502
+ });
503
+
504
+ this.controller.state.data = filtered;
505
+
506
+ if (this._localSortBaseline) {
507
+ this._localSortBaseline = filtered;
508
+ }
509
+
510
+ if (this._sort?.sorters?.length) {
511
+ this._sort.applyLocalSort();
512
+ } else {
513
+ void this.refreshBody();
514
+ }
515
+ }
516
+
517
+ private _matchesAllFilters(row: unknown, filters: FilterSnapshot[]): boolean {
518
+ for (const filter of filters) {
519
+ if (!this._matchesFilter(row, filter)) {
520
+ return false;
521
+ }
522
+ }
523
+ return true;
524
+ }
525
+
526
+ private _matchesFilter(row: unknown, filter: FilterSnapshot): boolean {
527
+ const { field, operator, value } = filter;
528
+ const rowValue = (row as Record<string, unknown>)?.[field];
529
+
530
+ if (value === '' || value == null) {
531
+ return true;
532
+ }
533
+
534
+ switch (operator?.toUpperCase?.() ?? '=') {
535
+ case '=':
536
+ case 'EQ':
537
+ return String(rowValue) === String(value);
538
+
539
+ case '!=':
540
+ case 'NE':
541
+ return String(rowValue) !== String(value);
542
+
543
+ case '>':
544
+ case 'GT':
545
+ return Number(rowValue) > Number(value);
546
+
547
+ case '>=':
548
+ case 'GTE':
549
+ return Number(rowValue) >= Number(value);
550
+
551
+ case '<':
552
+ case 'LT':
553
+ return Number(rowValue) < Number(value);
554
+
555
+ case '<=':
556
+ case 'LTE':
557
+ return Number(rowValue) <= Number(value);
558
+
559
+ case 'LIKE':
560
+ case 'CONTAINS': {
561
+ const pattern = String(value).toLowerCase();
562
+ const text = String(rowValue ?? '').toLowerCase();
563
+ return text.includes(pattern);
564
+ }
565
+
566
+ case 'STARTS':
567
+ case 'STARTSWITH': {
568
+ const pattern = String(value).toLowerCase();
569
+ const text = String(rowValue ?? '').toLowerCase();
570
+ return text.startsWith(pattern);
571
+ }
572
+
573
+ case 'ENDS':
574
+ case 'ENDSWITH': {
575
+ const pattern = String(value).toLowerCase();
576
+ const text = String(rowValue ?? '').toLowerCase();
577
+ return text.endsWith(pattern);
578
+ }
579
+
580
+ case 'BETWEEN': {
581
+ if (!Array.isArray(value) || value.length !== 2) {
582
+ return true;
583
+ }
584
+ const [from, to] = value;
585
+ const num = Number(rowValue);
586
+ if (from != null && num < Number(from)) return false;
587
+ if (to != null && num > Number(to)) return false;
588
+ return true;
589
+ }
590
+
591
+ case 'IN': {
592
+ const list = Array.isArray(value) ? value : [value];
593
+ return list.some(v => String(rowValue) === String(v));
594
+ }
595
+
596
+ case 'NOTIN': {
597
+ const list = Array.isArray(value) ? value : [value];
598
+ return !list.some(v => String(rowValue) === String(v));
599
+ }
600
+
601
+ case 'NULL':
602
+ case 'ISNULL':
603
+ return rowValue == null;
604
+
605
+ case 'NOTNULL':
606
+ case 'ISNOTNULL':
607
+ return rowValue != null;
608
+
609
+ default:
610
+ return String(rowValue ?? '').toLowerCase()
611
+ .includes(String(value).toLowerCase());
612
+ }
613
+ }
614
+
615
+ clearFilters(): void {
616
+ this._filtersInstance?.clear?.();
617
+ this._onFiltersChanged(null);
618
+ }
619
+
620
+ getFilters(): FilterSnapshot[] | null {
621
+ return this._lastFilterSnapshot ?? null;
622
+ }
623
+
624
+ // ==========================================================
625
+ // Remote Mode Public API
626
+ // ==========================================================
627
+
628
+ isRemoteMode(): boolean {
629
+ const mode = this.controller?.state?.mode;
630
+ return mode === 'remote' || mode === 'store';
631
+ }
632
+
633
+ isStoreMode(): boolean {
634
+ return this.controller?.state?.mode === 'store';
635
+ }
636
+
637
+ async load(params?: Record<string, unknown>): Promise<void> {
638
+ if (!this.isRemoteMode()) {
639
+ console.warn('[EzGrid] load() is only available in remote mode');
640
+ return;
641
+ }
642
+ return this.controller.load(params);
643
+ }
644
+
645
+ async reload(): Promise<void> {
646
+ return this.load();
647
+ }
648
+
649
+ getRemote(): EzGridRemote | null {
650
+ return this._remote;
651
+ }
652
+
653
+ // ==========================================================
654
+ // Export API
655
+ // ==========================================================
656
+
657
+ exportToCSV(options: ExportCSVOptions = {}): void {
658
+ const {
659
+ filename = 'export.csv',
660
+ includeHeaders = true,
661
+ separator = ',',
662
+ selectedOnly = false
663
+ } = options;
664
+
665
+ const csv = this._generateCSV({ includeHeaders, separator, selectedOnly });
666
+ this._downloadFile(csv, filename, 'text/csv;charset=utf-8;');
667
+ }
668
+
669
+ exportToExcel(options: ExportExcelOptions = {}): void {
670
+ const {
671
+ filename = 'export.xls',
672
+ includeHeaders = true,
673
+ selectedOnly = false
674
+ } = options;
675
+
676
+ const html = this._generateExcelHTML({ includeHeaders, selectedOnly });
677
+ this._downloadFile(html, filename, 'application/vnd.ms-excel');
678
+ }
679
+
680
+ private _generateCSV(options: { includeHeaders: boolean; separator: string; selectedOnly: boolean }): string {
681
+ const { includeHeaders, separator, selectedOnly } = options;
682
+ const columns = this.getVisibleColumns().filter(c =>
683
+ c.type !== 'selection' && c.type !== 'actions'
684
+ );
685
+
686
+ const data = this._getExportData(selectedOnly);
687
+ const lines: string[] = [];
688
+
689
+ if (includeHeaders) {
690
+ const headers = columns.map(c => this._escapeCSV(c.text ?? c.index));
691
+ lines.push(headers.join(separator));
692
+ }
693
+
694
+ for (const row of data) {
695
+ const values = columns.map(c => {
696
+ const value = (row as Record<string, unknown>)[c.index];
697
+ return this._escapeCSV(value ?? '');
698
+ });
699
+ lines.push(values.join(separator));
700
+ }
701
+
702
+ return lines.join('\n');
703
+ }
704
+
705
+ private _generateExcelHTML(options: { includeHeaders: boolean; selectedOnly: boolean }): string {
706
+ const { includeHeaders, selectedOnly } = options;
707
+ const columns = this.getVisibleColumns().filter(c =>
708
+ c.type !== 'selection' && c.type !== 'actions'
709
+ );
710
+
711
+ const data = this._getExportData(selectedOnly);
712
+
713
+ let html = '<html><head><meta charset="UTF-8"></head><body>';
714
+ html += '<table border="1">';
715
+
716
+ if (includeHeaders) {
717
+ html += '<thead><tr>';
718
+ for (const col of columns) {
719
+ html += `<th>${this._escapeHTML(col.text ?? col.index)}</th>`;
720
+ }
721
+ html += '</tr></thead>';
722
+ }
723
+
724
+ html += '<tbody>';
725
+ for (const row of data) {
726
+ html += '<tr>';
727
+ for (const col of columns) {
728
+ const value = (row as Record<string, unknown>)[col.index] ?? '';
729
+ html += `<td>${this._escapeHTML(String(value))}</td>`;
730
+ }
731
+ html += '</tr>';
732
+ }
733
+ html += '</tbody>';
734
+
735
+ html += '</table></body></html>';
736
+ return html;
737
+ }
738
+
739
+ private _getExportData(selectedOnly: boolean): unknown[] {
740
+ const data = this.controller?.state?.data ?? [];
741
+
742
+ if (!selectedOnly) return data;
743
+
744
+ return data.filter((row: any) => this.isSelected(row));
745
+ }
746
+
747
+ private _escapeCSV(value: unknown): string {
748
+ const str = String(value);
749
+
750
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
751
+ return `"${str.replace(/"/g, '""')}"`;
752
+ }
753
+ return str;
754
+ }
755
+
756
+ private _escapeHTML(str: string): string {
757
+ const div = document.createElement('div');
758
+ div.textContent = str;
759
+ return div.innerHTML;
760
+ }
761
+
762
+ private _downloadFile(content: string, filename: string, mimeType: string): void {
763
+ const blob = new Blob([content], { type: mimeType });
764
+ const url = URL.createObjectURL(blob);
765
+
766
+ const link = document.createElement('a');
767
+ link.href = url;
768
+ link.download = filename;
769
+ link.style.display = 'none';
770
+
771
+ document.body.appendChild(link);
772
+ link.click();
773
+ document.body.removeChild(link);
774
+
775
+ URL.revokeObjectURL(url);
776
+ }
777
+
778
+ // ==========================================================
779
+ // Parts Building
780
+ // ==========================================================
781
+
782
+ private _buildParts(): void {
783
+ this.config.items = this._parts.build();
784
+ this._parts.cleanupConfig();
785
+ }
786
+
787
+ // ==========================================================
788
+ // Store Mode Initialization
789
+ // ==========================================================
790
+
791
+ private _initStore(config: EzGridConfig): void {
792
+ const store = config.store as any;
793
+
794
+ if (!store || typeof store.load !== 'function') {
795
+ console.warn('[EzGrid] Invalid store provided');
796
+ return;
797
+ }
798
+
799
+ this._store = store;
800
+ this.controller.state.mode = 'store';
801
+ this.controller.state.pageSize = store.state.pageSize;
802
+ this.controller.state.page = store.state.page;
803
+
804
+ this._storeUnsubscribers.push(
805
+ store.on('datachange', (data: unknown) => {
806
+ this.controller.state.data = data as RowData[];
807
+ this.controller.state.total = store.state.total;
808
+ this.controller.emit('datachange', data);
809
+ }),
810
+ store.on('loadingchange', (loading: unknown) => {
811
+ this.controller.state.loading = loading as boolean;
812
+ this._onLoadingChanged(loading as boolean);
813
+ }),
814
+ store.on('pagechange', (payload: unknown) => {
815
+ const { page, pageSize } = payload as { page: number; pageSize: number };
816
+ this.controller.state.page = page;
817
+ this.controller.state.pageSize = pageSize;
818
+ })
819
+ );
820
+
821
+ const originalSetPage = this.controller.setPage.bind(this.controller);
822
+ this.controller.setPage = (page: number) => {
823
+ if (this._store) {
824
+ this._store.setPage(page);
825
+ } else {
826
+ originalSetPage(page);
827
+ }
828
+ };
829
+
830
+ const originalSetPageSize = this.controller.setPageSize.bind(this.controller);
831
+ this.controller.setPageSize = (size: number) => {
832
+ if (this._store) {
833
+ this._store.setPageSize(size);
834
+ } else {
835
+ originalSetPageSize(size);
836
+ }
837
+ };
838
+
839
+ this.controller.load = () => {
840
+ if (this._store) {
841
+ return this._store.load();
842
+ }
843
+ return Promise.resolve();
844
+ };
845
+
846
+ this.controller.reload = () => {
847
+ if (this._store) {
848
+ return this._store.reload();
849
+ }
850
+ return Promise.resolve();
851
+ };
852
+
853
+ if (config.autoLoad !== false && store.state.data.length === 0 && !store.state.loading) {
854
+ this._autoLoadPending = true;
855
+ }
856
+ }
857
+
858
+ getStore(): any {
859
+ return this._store;
860
+ }
861
+
862
+ // ==========================================================
863
+ // Remote Mode Initialization
864
+ // ==========================================================
865
+
866
+ private _initRemote(config: EzGridConfig): void {
867
+ this._remote = new EzGridRemote(this, config.remote!);
868
+
869
+ if (!this._remote.isEnabled()) {
870
+ console.warn('[EzGrid] Remote config provided but not valid');
871
+ return;
872
+ }
873
+
874
+ if (config.pageSize) {
875
+ this.controller.state.pageSize = config.pageSize;
876
+ }
877
+
878
+ const transport = async (params: Record<string, unknown>) => {
879
+ return this._remote!.load(params);
880
+ };
881
+
882
+ this.controller.setRemoteConfig({
883
+ transport,
884
+ autoLoad: config.autoLoad === true,
885
+ pageSize: config.pageSize ?? 25
886
+ });
887
+
888
+ if (config.autoLoad === true) {
889
+ this._autoLoadPending = true;
890
+ }
891
+ }
892
+
893
+ private _initController(): void {
894
+ if (!this.stateful) {
895
+ this.controller = createEzGridController(this);
896
+ return;
897
+ }
898
+
899
+ if (!this.config.id) {
900
+ throw new EzError({
901
+ code: 'EZ_GRID_STATE_ID_REQUIRED',
902
+ message: 'Stateful EzGrid requires a unique "id"'
903
+ });
904
+ }
905
+
906
+ if (this._persistence.hasRegistryEntry()) {
907
+ this.controller = this._persistence.getRegistryEntry();
908
+
909
+ const entry = this._persistence.getRegistryEntry();
910
+ if (entry?._columnState) {
911
+ this._columns.restore(entry._columnState);
912
+ }
913
+
914
+ if (entry?._sortState) {
915
+ this._sort.restore(entry._sortState);
916
+ }
917
+
918
+ if (entry?._selectionState) {
919
+ this._applySelectionStateSnapshot(entry._selectionState);
920
+ }
921
+
922
+ return;
923
+ }
924
+
925
+ this.controller = createEzGridController(this);
926
+ this._persistence.setRegistryEntry(this.controller);
927
+
928
+ const stored = this._persistence.load();
929
+ if (stored) {
930
+ if (stored.columnState) {
931
+ this._columns.restore(stored.columnState);
932
+ this.controller._columnState = stored.columnState;
933
+ }
934
+
935
+ if (stored.sortState) {
936
+ this._applySortStateSnapshot(stored.sortState);
937
+ this.controller._sortState = stored.sortState;
938
+ }
939
+
940
+ if (stored.selectionState) {
941
+ this._applySelectionStateSnapshot(stored.selectionState);
942
+ this.controller._selectionState = stored.selectionState;
943
+ }
944
+ }
945
+ }
946
+
947
+ private async _ensureOverRulesApplied(): Promise<void> {
948
+ if (this._overRulesApplied) return;
949
+
950
+ if (!this.config.overRules) {
951
+ this._overRulesApplied = true;
952
+ return;
953
+ }
954
+
955
+ const rules = await ez.getGridBehavior(this.config.overRules);
956
+
957
+ Object.keys(rules).forEach(key => {
958
+ (this.controller as Record<string, unknown>)[key] = rules[key];
959
+ });
960
+
961
+ this.controller.onInit?.(this);
962
+ this._overRulesApplied = true;
963
+ }
964
+
965
+ private async _bindData(): Promise<void> {
966
+ if (this._store) {
967
+ if (this._store.state.data.length > 0) {
968
+ this.controller.state.data = this._store.state.data;
969
+ this.controller.state.total = this._store.state.total;
970
+ }
971
+ return;
972
+ }
973
+
974
+ let data: RowData[] = [];
975
+
976
+ if (Array.isArray(this.config.data)) {
977
+ data = this.config.data as RowData[];
978
+ } else {
979
+ const bind = this.config.bind;
980
+ if (bind && bind.data) {
981
+ const [controllerName, prop] = bind.data.split(':');
982
+ const controller = ez.getController(controllerName);
983
+
984
+ if (!controller || !controller.state) {
985
+ throw new EzError({
986
+ code: 'EZ_GRID_BIND_001',
987
+ source: 'grid',
988
+ message: `Invalid bind.data "${bind.data}"`
989
+ });
990
+ }
991
+
992
+ const boundData = controller.state[prop] ?? [];
993
+ data = Array.isArray(boundData) ? boundData : [];
994
+ }
995
+ }
996
+
997
+ if (this.config.model) {
998
+ await ez._loader.resolveModel(this.config.model);
999
+ const model = ez.getModelSync(this.config.model);
1000
+ this.controller._model = model;
1001
+
1002
+ if (data.length > 0 && model) {
1003
+ data = model.processAll(data);
1004
+ }
1005
+ }
1006
+
1007
+ this.controller.state.data = data;
1008
+ this.controller.state.total = data.length;
1009
+ }
1010
+
1011
+ async _onControllerDataChange(): Promise<void> {
1012
+ this._localSortBaseline = null;
1013
+ this._hasLocalSortBaseline = false;
1014
+ this._lifecycle.onControllerDataChange();
1015
+ }
1016
+
1017
+ async suspendRefresh(fn: () => Promise<void> | void): Promise<void> {
1018
+ await this._lifecycle.suspend(fn);
1019
+ }
1020
+
1021
+ // ==========================================================
1022
+ // Refresh Execution
1023
+ // ==========================================================
1024
+
1025
+ async _refreshBodyInternal(): Promise<void> {
1026
+ if (!this.el) return;
1027
+
1028
+ const fullData = this.controller.state.data ?? [];
1029
+
1030
+ if (typeof this.controller.beforeDataRender === 'function') {
1031
+ try {
1032
+ this.controller.beforeDataRender(this, fullData);
1033
+ } catch (err) {
1034
+ console.error('[EzGrid] beforeDataRender error', err);
1035
+ }
1036
+ }
1037
+
1038
+ try {
1039
+ let displayData = fullData;
1040
+
1041
+ if (this.controller.state.mode !== 'remote') {
1042
+ const { page, pageSize } = this.controller.state;
1043
+ this.controller.state.total = fullData.length;
1044
+
1045
+ if (page && pageSize) {
1046
+ const start = (page - 1) * pageSize;
1047
+ const end = start + pageSize;
1048
+ displayData = fullData.slice(start, end);
1049
+ }
1050
+ }
1051
+
1052
+ this.config.data = displayData;
1053
+
1054
+ const body = this._bodyInstance;
1055
+ if (body?.refreshData) {
1056
+ await body.refreshData(displayData);
1057
+
1058
+ this._lifecycle.markHydrated();
1059
+ this._footerInstance?.refresh?.();
1060
+
1061
+ if (typeof this.controller.afterDataRender === 'function') {
1062
+ try {
1063
+ this.controller.afterDataRender(this, fullData);
1064
+ } catch (err) {
1065
+ console.error('[EzGrid] afterDataRender error', err);
1066
+ }
1067
+ }
1068
+ }
1069
+ } finally {
1070
+ // Replay/pending refresh logic is owned by EzGridLifecycle
1071
+ }
1072
+ }
1073
+
1074
+ // ==========================================================
1075
+ // Internal Helpers
1076
+ // ==========================================================
1077
+
1078
+ _getRowKey(row: unknown): string | number | null {
1079
+ const key = this.config.rowKey;
1080
+ if (typeof key === 'function') {
1081
+ return key(row);
1082
+ }
1083
+ return (row as Record<string, unknown>)?.[key as string] as string | number ?? null;
1084
+ }
1085
+ }