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,273 @@
1
+ // ==========================================================
2
+ // EzGridNormalizers - Unit Tests
3
+ // ==========================================================
4
+ // Para correr: npm test
5
+ // Para correr una vez: npm run test:run
6
+ // Para correr solo este archivo: npm test EzGridNormalizers
7
+ // ==========================================================
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { EzGridNormalizers } from './EzGridNormalizers.js';
11
+
12
+ // ==========================================================
13
+ // normalizeHeader
14
+ // ==========================================================
15
+
16
+ describe('EzGridNormalizers.normalizeHeader', () => {
17
+
18
+ it('should return false when header is false', () => {
19
+ const result = EzGridNormalizers.normalizeHeader(false);
20
+ expect(result).toBe(false);
21
+ });
22
+
23
+ it('should return default config when header is true', () => {
24
+ const result = EzGridNormalizers.normalizeHeader(true);
25
+ expect(result).toEqual({ align: 'left' });
26
+ });
27
+
28
+ it('should return default config when header is null/undefined', () => {
29
+ expect(EzGridNormalizers.normalizeHeader(null)).toEqual({ align: 'left' });
30
+ expect(EzGridNormalizers.normalizeHeader(undefined)).toEqual({ align: 'left' });
31
+ });
32
+
33
+ it('should preserve custom align from config', () => {
34
+ const result = EzGridNormalizers.normalizeHeader({ align: 'center' });
35
+ expect(result.align).toBe('center');
36
+ });
37
+
38
+ it('should preserve all header properties', () => {
39
+ const result = EzGridNormalizers.normalizeHeader({
40
+ align: 'right',
41
+ cls: 'my-header',
42
+ height: 50,
43
+ visible: false
44
+ });
45
+
46
+ expect(result).toEqual({
47
+ align: 'right',
48
+ cls: 'my-header',
49
+ height: 50,
50
+ visible: false
51
+ });
52
+ });
53
+
54
+ });
55
+
56
+ // ==========================================================
57
+ // normalizeColumns
58
+ // ==========================================================
59
+
60
+ describe('EzGridNormalizers.normalizeColumns', () => {
61
+
62
+ it('should normalize basic columns', () => {
63
+ const columns = [
64
+ { index: 'name', text: 'Name' },
65
+ { index: 'email', text: 'Email' }
66
+ ];
67
+
68
+ const result = EzGridNormalizers.normalizeColumns(columns);
69
+
70
+ expect(result).toHaveLength(2);
71
+ expect(result[0].index).toBe('name');
72
+ expect(result[0].text).toBe('Name');
73
+ expect(result[0].flex).toBe(1); // default flex
74
+ expect(result[0].sortable).toBe(true); // default sortable
75
+ });
76
+
77
+ it('should set default flex to 1', () => {
78
+ const columns = [{ index: 'name' }];
79
+ const result = EzGridNormalizers.normalizeColumns(columns);
80
+
81
+ expect(result[0].flex).toBe(1);
82
+ });
83
+
84
+ it('should preserve custom flex value', () => {
85
+ const columns = [{ index: 'name', flex: 3 }];
86
+ const result = EzGridNormalizers.normalizeColumns(columns);
87
+
88
+ expect(result[0].flex).toBe(3);
89
+ });
90
+
91
+ it('should preserve fixed width', () => {
92
+ const columns = [{ index: 'name', width: 200 }];
93
+ const result = EzGridNormalizers.normalizeColumns(columns);
94
+
95
+ expect(result[0].width).toBe(200);
96
+ });
97
+
98
+ it('should handle actions column', () => {
99
+ const columns = [
100
+ { index: 'name' },
101
+ { type: 'actions', actions: [{ icon: '✏️' }] }
102
+ ];
103
+
104
+ const result = EzGridNormalizers.normalizeColumns(columns);
105
+
106
+ expect(result[1].type).toBe('actions');
107
+ expect(result[1].width).toBe(96); // default actions width
108
+ expect(result[1].actions).toHaveLength(1);
109
+ expect(result[1].header.align).toBe('center');
110
+ });
111
+
112
+ it('should use custom width for actions column', () => {
113
+ const columns = [
114
+ { type: 'actions', width: 150, actions: [] }
115
+ ];
116
+
117
+ const result = EzGridNormalizers.normalizeColumns(columns);
118
+
119
+ expect(result[0].width).toBe(150);
120
+ });
121
+
122
+ // ----- Selection Column -----
123
+
124
+ it('should NOT add selection column when mode is single', () => {
125
+ const columns = [{ index: 'name' }];
126
+ const result = EzGridNormalizers.normalizeColumns(columns, {
127
+ selectionMode: 'single'
128
+ });
129
+
130
+ expect(result).toHaveLength(1);
131
+ expect(result[0].type).not.toBe('selection');
132
+ });
133
+
134
+ it('should auto-add selection column when mode is multi', () => {
135
+ const columns = [{ index: 'name' }];
136
+ const result = EzGridNormalizers.normalizeColumns(columns, {
137
+ selectionMode: 'multi'
138
+ });
139
+
140
+ expect(result).toHaveLength(2);
141
+ expect(result[0].type).toBe('selection');
142
+ expect(result[0].width).toBe(36); // default width
143
+ });
144
+
145
+ it('should use custom selection width', () => {
146
+ const columns = [{ index: 'name' }];
147
+ const result = EzGridNormalizers.normalizeColumns(columns, {
148
+ selectionMode: 'multi',
149
+ selectionWidth: 50
150
+ });
151
+
152
+ expect(result[0].type).toBe('selection');
153
+ expect(result[0].width).toBe(50);
154
+ });
155
+
156
+ it('should filter out manually added selection columns', () => {
157
+ const columns = [
158
+ { type: 'selection', width: 40 }, // manual - should be ignored
159
+ { index: 'name' }
160
+ ];
161
+
162
+ const result = EzGridNormalizers.normalizeColumns(columns, {
163
+ selectionMode: 'multi',
164
+ selectionWidth: 36
165
+ });
166
+
167
+ // Should have 2: auto-added selection + name
168
+ expect(result).toHaveLength(2);
169
+ expect(result[0].type).toBe('selection');
170
+ expect(result[0].width).toBe(36); // uses selectionWidth, not manual 40
171
+ });
172
+
173
+ // ----- Wrap -----
174
+
175
+ it('should normalize wrap: true to 3 lines', () => {
176
+ const columns = [{ index: 'description', wrap: true }];
177
+ const result = EzGridNormalizers.normalizeColumns(columns);
178
+
179
+ expect(result[0].wrap).toBe(3);
180
+ });
181
+
182
+ it('should preserve numeric wrap value', () => {
183
+ const columns = [{ index: 'description', wrap: 5 }];
184
+ const result = EzGridNormalizers.normalizeColumns(columns);
185
+
186
+ expect(result[0].wrap).toBe(5);
187
+ });
188
+
189
+ it('should set wrap to false when not specified', () => {
190
+ const columns = [{ index: 'name' }];
191
+ const result = EzGridNormalizers.normalizeColumns(columns);
192
+
193
+ expect(result[0].wrap).toBe(false);
194
+ });
195
+
196
+ // ----- Sortable -----
197
+
198
+ it('should be sortable by default', () => {
199
+ const columns = [{ index: 'name' }];
200
+ const result = EzGridNormalizers.normalizeColumns(columns);
201
+
202
+ expect(result[0].sortable).toBe(true);
203
+ });
204
+
205
+ it('should disable sort when sort: false', () => {
206
+ const columns = [{ index: 'name', sort: false }];
207
+ const result = EzGridNormalizers.normalizeColumns(columns);
208
+
209
+ expect(result[0].sortable).toBe(false);
210
+ });
211
+
212
+ // ----- Header Align -----
213
+
214
+ it('should use column header align', () => {
215
+ const columns = [{ index: 'price', header: { align: 'right' } }];
216
+ const result = EzGridNormalizers.normalizeColumns(columns);
217
+
218
+ expect(result[0].header.align).toBe('right');
219
+ });
220
+
221
+ it('should use global headerAlign as fallback', () => {
222
+ const columns = [{ index: 'name' }];
223
+ const result = EzGridNormalizers.normalizeColumns(columns, {
224
+ headerAlign: 'center'
225
+ });
226
+
227
+ expect(result[0].header.align).toBe('center');
228
+ });
229
+
230
+ it('should default to left align', () => {
231
+ const columns = [{ index: 'name' }];
232
+ const result = EzGridNormalizers.normalizeColumns(columns);
233
+
234
+ expect(result[0].header.align).toBe('left');
235
+ });
236
+
237
+ });
238
+
239
+ // ==========================================================
240
+ // normalizeRowKey
241
+ // ==========================================================
242
+
243
+ describe('EzGridNormalizers.normalizeRowKey', () => {
244
+
245
+ it('should return function as-is', () => {
246
+ const fn = (row) => row.customId;
247
+ const result = EzGridNormalizers.normalizeRowKey(fn);
248
+
249
+ expect(result).toBe(fn);
250
+ });
251
+
252
+ it('should convert string to accessor function', () => {
253
+ const result = EzGridNormalizers.normalizeRowKey('userId');
254
+
255
+ expect(typeof result).toBe('function');
256
+ expect(result({ userId: 123 })).toBe(123);
257
+ });
258
+
259
+ it('should default to id field', () => {
260
+ const result = EzGridNormalizers.normalizeRowKey(undefined);
261
+
262
+ expect(typeof result).toBe('function');
263
+ expect(result({ id: 456 })).toBe(456);
264
+ });
265
+
266
+ it('should handle null row gracefully', () => {
267
+ const result = EzGridNormalizers.normalizeRowKey('id');
268
+
269
+ expect(result(null)).toBe(undefined);
270
+ expect(result(undefined)).toBe(undefined);
271
+ });
272
+
273
+ });
@@ -0,0 +1,162 @@
1
+ // EzGrid/state/EzGridNormalizers.ts
2
+
3
+ import type {
4
+ HeaderAlign,
5
+ HeaderConfig,
6
+ ColumnConfig,
7
+ NormalizedColumn,
8
+ SelectionMode,
9
+ RowData,
10
+ RowKeyFn
11
+ } from '../types.js';
12
+
13
+ export interface NormalizeColumnsOptions {
14
+ selectionMode?: SelectionMode;
15
+ selectionWidth?: number;
16
+ headerAlign?: HeaderAlign;
17
+ }
18
+
19
+ // Re-export types for backwards compatibility
20
+ export type {
21
+ HeaderAlign,
22
+ HeaderConfig,
23
+ ColumnConfig,
24
+ NormalizedColumn
25
+ } from '../types.js';
26
+
27
+ export type RowKeyFunction = RowKeyFn;
28
+
29
+ export class EzGridNormalizers {
30
+
31
+ // ==========================================================
32
+ // Header Normalization
33
+ // ==========================================================
34
+
35
+ static normalizeHeader(header: boolean | HeaderConfig | null | undefined): HeaderConfig | false {
36
+ if (header === false) return false;
37
+
38
+ if (header === true || header == null) {
39
+ return {
40
+ align: 'left'
41
+ };
42
+ }
43
+
44
+ if (typeof header === 'object') {
45
+ return {
46
+ align: header.align ?? 'left',
47
+ cls: header.cls,
48
+ height: header.height,
49
+ visible: header.visible
50
+ };
51
+ }
52
+
53
+ return { align: 'left' };
54
+ }
55
+
56
+ // ==========================================================
57
+ // Columns Normalization
58
+ // ==========================================================
59
+
60
+ static normalizeColumns(columns: ColumnConfig[], options: NormalizeColumnsOptions = {}): NormalizedColumn[] {
61
+ const { selectionMode, selectionWidth = 36, headerAlign } = options;
62
+
63
+ // Filter out manually added selection columns (we'll add it automatically)
64
+ const filteredColumns = columns.filter(col => col.type !== 'selection');
65
+
66
+ const normalizedColumns: NormalizedColumn[] = filteredColumns.map((col, index) => {
67
+ if (col.type === 'actions') {
68
+ return {
69
+ type: 'actions',
70
+ text: '',
71
+ flex: 0,
72
+ render: null,
73
+ sortable: false,
74
+ filter: null,
75
+ width: col.width ?? 96,
76
+ actions: Array.isArray(col.actions) ? col.actions : [],
77
+ header: {
78
+ align: 'center' as HeaderAlign
79
+ },
80
+ wrap: false,
81
+ resizable: false,
82
+ reorderable: false,
83
+ summary: null,
84
+ _id: `actions-${index}`
85
+ };
86
+ }
87
+
88
+ // Normalize filter to FilterConfig | null
89
+ let normalizedFilter: { operator?: string; itemRender?: unknown } | null = null;
90
+ if (col.filter === true) {
91
+ normalizedFilter = {};
92
+ } else if (typeof col.filter === 'string') {
93
+ normalizedFilter = { operator: col.filter };
94
+ } else if (col.filter && typeof col.filter === 'object') {
95
+ normalizedFilter = col.filter;
96
+ }
97
+
98
+ // Normalize summary to SummaryConfig | null
99
+ let normalizedSummary: { type: 'count' | 'sum' | 'avg' | 'average' | 'min' | 'max' } | null = null;
100
+ if (typeof col.summary === 'string') {
101
+ normalizedSummary = { type: col.summary as 'count' | 'sum' | 'avg' | 'average' | 'min' | 'max' };
102
+ } else if (col.summary && typeof col.summary === 'object') {
103
+ normalizedSummary = col.summary;
104
+ }
105
+
106
+ return {
107
+ index: col.index,
108
+ type: col.type,
109
+ text: col.text ?? '',
110
+ flex: col.flex ?? 1,
111
+ render: typeof col.render === 'function' ? col.render : null,
112
+ header: {
113
+ align: col.header?.align ?? headerAlign ?? 'left'
114
+ },
115
+ width: col.width,
116
+ sortable: col.sort !== false,
117
+ filter: normalizedFilter,
118
+ wrap: col.wrap === true ? 3 : (typeof col.wrap === 'number' ? col.wrap : false),
119
+ resizable: col.resizable !== false,
120
+ reorderable: col.reorderable !== false,
121
+ summary: normalizedSummary,
122
+ _id: col.index || `col-${index}`
123
+ };
124
+ });
125
+
126
+ // Auto-add selection column for multi mode
127
+ if (selectionMode === 'multi') {
128
+ normalizedColumns.unshift({
129
+ type: 'selection',
130
+ text: '',
131
+ flex: 0,
132
+ render: null,
133
+ sortable: false,
134
+ filter: null,
135
+ width: selectionWidth,
136
+ header: { align: 'center' },
137
+ wrap: false,
138
+ resizable: false,
139
+ reorderable: false,
140
+ summary: null,
141
+ _id: 'selection'
142
+ });
143
+ }
144
+
145
+ return normalizedColumns;
146
+ }
147
+
148
+ // ==========================================================
149
+ // RowKey Normalization
150
+ // ==========================================================
151
+
152
+ static normalizeRowKey(rowKey: string | RowKeyFunction | undefined): RowKeyFunction {
153
+ if (typeof rowKey === 'function') return rowKey;
154
+
155
+ if (typeof rowKey === 'string') {
156
+ return (row: any) => row?.[rowKey];
157
+ }
158
+
159
+ // fallback to 'id' field
160
+ return (row: any) => row?.id;
161
+ }
162
+ }
@@ -0,0 +1,233 @@
1
+ // EzGrid/state/EzGridParts.ts
2
+
3
+ import type {
4
+ NormalizedColumn,
5
+ EzGridController,
6
+ EzGridSelectionRef,
7
+ RowData,
8
+ RowKeyFn,
9
+ RowCallback,
10
+ RowContextMenuCallback,
11
+ TitleBarContext,
12
+ ToolConfig,
13
+ EzGridConfig,
14
+ HeaderConfig,
15
+ FilterMode,
16
+ EzGridHeaderRef,
17
+ EzGridFiltersRef,
18
+ EzGridBodyRef,
19
+ EzGridFooterRef,
20
+ EzGridRemoteRef
21
+ } from '../types.js';
22
+ import type { EzGridSelection } from './EzGridSelection.js';
23
+
24
+ export interface ComponentConfig {
25
+ eztype: string;
26
+ grid?: EzGridPartsRef;
27
+ _ezAfterRender?: (el: HTMLElement, instance: unknown) => void;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ export interface EzGridPartsRef {
32
+ config: {
33
+ titleBar?: (ctx: TitleBarContext) => ComponentConfig | null;
34
+ title?: string | ((ctx: TitleBarContext) => unknown);
35
+ tools?: ToolConfig[] | ((ctx: TitleBarContext) => ToolConfig[]);
36
+ bind?: { data?: string; title?: string };
37
+ id?: string;
38
+ controller?: string;
39
+ overRules?: string;
40
+ data?: RowData[];
41
+ rowHeight?: number;
42
+ };
43
+ controller: EzGridController | null;
44
+ header: HeaderConfig | false;
45
+ selection: EzGridSelection;
46
+ rowKey: RowKeyFn;
47
+ headerInstance?: EzGridHeaderRef;
48
+ _filtersInstance?: EzGridFiltersRef;
49
+ _bodyInstance?: EzGridBodyRef;
50
+ _footerInstance?: EzGridFooterRef;
51
+ _remote?: EzGridRemoteRef;
52
+ _rowClickHandler?: RowCallback | null;
53
+ _rowDoubleClickHandler?: RowCallback | null;
54
+ _rowContextMenuHandler?: RowContextMenuCallback | null;
55
+ getSelection: () => string[];
56
+ isSelected: (row: RowData) => boolean;
57
+ getVisibleColumns: () => NormalizedColumn[];
58
+ isRemoteMode: () => boolean;
59
+ }
60
+
61
+ // Re-export for backwards compatibility
62
+ export type { TitleBarContext } from '../types.js';
63
+
64
+ export class EzGridParts {
65
+ grid: EzGridPartsRef;
66
+
67
+ constructor(grid: EzGridPartsRef) {
68
+ this.grid = grid;
69
+ }
70
+
71
+ // ==========================================================
72
+ // Build All Parts
73
+ // ==========================================================
74
+
75
+ build(): ComponentConfig[] {
76
+ const items: ComponentConfig[] = [];
77
+
78
+ items.push(...this._buildTitleBar());
79
+ items.push(...this._buildHeader());
80
+ items.push(...this._buildFilters());
81
+ items.push(...this._buildBody());
82
+ items.push(...this._buildFooter());
83
+
84
+ return items;
85
+ }
86
+
87
+ // ==========================================================
88
+ // TitleBar
89
+ // ==========================================================
90
+
91
+ private _buildTitleBar(): ComponentConfig[] {
92
+ const grid = this.grid;
93
+ const config = grid.config;
94
+
95
+ const ctx = {
96
+ grid: grid as any,
97
+ controller: grid.controller,
98
+ selection: {
99
+ keys: grid.getSelection(),
100
+ rows: grid.controller?.state?.data?.filter(row =>
101
+ grid.isSelected(row)
102
+ ) ?? [],
103
+ count: grid.getSelection().length
104
+ },
105
+ state: grid.controller?.state ?? null,
106
+ bind: config.bind ?? null,
107
+ config: config as any
108
+ } as TitleBarContext;
109
+
110
+ // Full override of titlebar
111
+ if (typeof config.titleBar === 'function') {
112
+ const res = config.titleBar(ctx);
113
+ return res ? [res] : [];
114
+ }
115
+
116
+ // Default titlebar with partial overrides
117
+ if (config.title || config.tools || config.bind?.title) {
118
+ return [{
119
+ eztype: 'EzGridTitleBar',
120
+ title: config.title,
121
+ tools: config.tools,
122
+ bind: config.bind,
123
+ grid: grid,
124
+ titleOverride: typeof config.title === 'function'
125
+ ? config.title
126
+ : null,
127
+ toolsOverride: typeof config.tools === 'function'
128
+ ? config.tools
129
+ : null,
130
+ ctx
131
+ }];
132
+ }
133
+
134
+ return [];
135
+ }
136
+
137
+ // ==========================================================
138
+ // Header
139
+ // ==========================================================
140
+
141
+ private _buildHeader(): ComponentConfig[] {
142
+ const grid = this.grid;
143
+
144
+ if (grid.header === false) return [];
145
+
146
+ return [{
147
+ eztype: 'EzGridHeader',
148
+ header: grid.header,
149
+ columns: () => grid.getVisibleColumns(),
150
+ grid: grid,
151
+ _ezAfterRender: (el: HTMLElement, instance: any) => {
152
+ grid.headerInstance = instance;
153
+ }
154
+ }];
155
+ }
156
+
157
+ // ==========================================================
158
+ // Filters
159
+ // ==========================================================
160
+
161
+ private _buildFilters(): ComponentConfig[] {
162
+ const grid = this.grid;
163
+
164
+ // Get filter mode from remote config (only applies to remote mode)
165
+ const filterMode = grid._remote?.config?.filter ?? 'onInput';
166
+
167
+ return [{
168
+ eztype: 'EzGridFilters',
169
+ grid: grid,
170
+ columns: () => grid.getVisibleColumns(),
171
+ filterMode: grid.isRemoteMode() ? filterMode : 'onInput',
172
+ _ezAfterRender: (el: HTMLElement, instance: any) => {
173
+ grid._filtersInstance = instance;
174
+ }
175
+ }];
176
+ }
177
+
178
+ // ==========================================================
179
+ // Body
180
+ // ==========================================================
181
+
182
+ private _buildBody(): ComponentConfig[] {
183
+ const grid = this.grid;
184
+ const config = grid.config;
185
+
186
+ return [{
187
+ eztype: 'EzGridBody',
188
+ grid: grid,
189
+ controller: grid.controller,
190
+ _ezAfterRender: (el: HTMLElement, instance: any) => {
191
+ grid._bodyInstance = instance;
192
+ },
193
+ overRules: config.overRules,
194
+ columns: () => grid.getVisibleColumns(),
195
+ rowKey: grid.rowKey,
196
+ selection: grid.selection,
197
+ bind: config.bind,
198
+ data: config.data,
199
+ rowHeight: config.rowHeight || null,
200
+ // Use resolved handlers from listenTo (supports string callbacks)
201
+ onRowClick: grid._rowClickHandler || null,
202
+ onRowDoubleClick: grid._rowDoubleClickHandler || null,
203
+ onRowContextMenu: grid._rowContextMenuHandler || null,
204
+ flex: 1
205
+ }];
206
+ }
207
+
208
+ // ==========================================================
209
+ // Footer
210
+ // ==========================================================
211
+
212
+ private _buildFooter(): ComponentConfig[] {
213
+ const grid = this.grid;
214
+
215
+ return [{
216
+ eztype: 'EzGridFooter',
217
+ grid: grid,
218
+ _ezAfterRender: (el: HTMLElement, instance: any) => {
219
+ grid._footerInstance = instance;
220
+ }
221
+ }];
222
+ }
223
+
224
+ // ==========================================================
225
+ // Cleanup Config
226
+ // ==========================================================
227
+
228
+ cleanupConfig(): void {
229
+ delete (this.grid.config as any).title;
230
+ delete (this.grid.config as any).tools;
231
+ delete (this.grid.config as any).columns;
232
+ }
233
+ }