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,335 @@
1
+ // EzGrid/state/EzGridRemote.ts
2
+
3
+ import type {
4
+ EzGridController,
5
+ SortSnapshot,
6
+ FilterSnapshot,
7
+ RowData,
8
+ RemoteApiConfig,
9
+ RemoteSourceConfig,
10
+ RemoteListenTo,
11
+ RemoteConfigInput,
12
+ TransportParams,
13
+ TransportResult,
14
+ BeforeLoadContext,
15
+ AfterLoadContext,
16
+ ErrorContext,
17
+ FilterMode
18
+ } from '../types.js';
19
+
20
+ declare const ez: {
21
+ _api: {
22
+ request: (url: string, options?: RequestInit) => Promise<unknown>;
23
+ };
24
+ getController: (name: string) => unknown;
25
+ };
26
+
27
+ export interface NormalizedRemoteConfig {
28
+ api: RemoteApiConfig;
29
+ source: {
30
+ dataPath: string;
31
+ countPath: string;
32
+ };
33
+ filter: FilterMode;
34
+ listenTo: RemoteListenTo;
35
+ }
36
+
37
+ export interface RemoteParams {
38
+ page: number;
39
+ pageSize: number;
40
+ sort: SortSnapshot | null;
41
+ filters: FilterSnapshot[] | null;
42
+ }
43
+
44
+ export interface RemoteResult {
45
+ data: RowData[];
46
+ total: number;
47
+ }
48
+
49
+ export interface EzGridRemoteRef {
50
+ config?: {
51
+ controller?: string;
52
+ };
53
+ controller?: EzGridController;
54
+ }
55
+
56
+ // Re-export types for backwards compatibility
57
+ export type {
58
+ RemoteApiConfig,
59
+ RemoteSourceConfig,
60
+ RemoteListenTo,
61
+ RemoteConfigInput,
62
+ BeforeLoadContext,
63
+ AfterLoadContext,
64
+ ErrorContext
65
+ } from '../types.js';
66
+
67
+ export class EzGridRemote {
68
+ grid: EzGridRemoteRef;
69
+ config: NormalizedRemoteConfig | null;
70
+ private _isLoading: boolean;
71
+
72
+ constructor(grid: EzGridRemoteRef, config: RemoteConfigInput) {
73
+ this.grid = grid;
74
+ this.config = this._normalizeConfig(config);
75
+ this._isLoading = false;
76
+ }
77
+
78
+ // ==========================================================
79
+ // Callback Resolution
80
+ // ==========================================================
81
+
82
+ private _resolveCallback<T extends (...args: any[]) => any>(
83
+ callback: T | string | undefined
84
+ ): T | null {
85
+ if (typeof callback === 'function') {
86
+ return callback;
87
+ }
88
+
89
+ if (typeof callback === 'string') {
90
+ // Format: "ControllerName:functionName"
91
+ if (callback.includes(':')) {
92
+ const [controllerName, fnName] = callback.split(':');
93
+ const controller = ez.getController(controllerName) as Record<string, unknown>;
94
+
95
+ if (controller && typeof controller[fnName] === 'function') {
96
+ return (controller[fnName] as Function).bind(controller);
97
+ }
98
+
99
+ console.warn(`[EzGridRemote] Could not resolve ${callback}`);
100
+ return null;
101
+ }
102
+
103
+ // Format: "functionName" - use component's controller from config
104
+ const controllerName = this.grid?.config?.controller;
105
+ if (controllerName) {
106
+ const controller = ez.getController(controllerName) as Record<string, unknown>;
107
+ if (controller && typeof controller[callback] === 'function') {
108
+ return (controller[callback] as Function).bind(controller);
109
+ }
110
+ }
111
+
112
+ console.warn(`[EzGridRemote] Could not resolve ${callback} in ${controllerName || 'undefined'} controller`);
113
+ return null;
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ // ==========================================================
120
+ // Configuration
121
+ // ==========================================================
122
+
123
+ private _normalizeConfig(config: RemoteConfigInput | null | undefined): NormalizedRemoteConfig | null {
124
+ if (!config) return null;
125
+
126
+ const api: RemoteApiConfig = typeof config.api === 'string'
127
+ ? { read: config.api, create: config.api, update: config.api, delete: config.api }
128
+ : config.api || {};
129
+
130
+ return {
131
+ api,
132
+ source: {
133
+ dataPath: config.source?.dataPath ?? 'data',
134
+ countPath: config.source?.countPath ?? 'count'
135
+ },
136
+ filter: config.filter ?? 'onInput',
137
+ listenTo: config.listenTo || {}
138
+ };
139
+ }
140
+
141
+ isEnabled(): boolean {
142
+ return this.config !== null && !!this.config.api?.read;
143
+ }
144
+
145
+ // ==========================================================
146
+ // URL Building
147
+ // ==========================================================
148
+
149
+ private _buildReadUrl(params: RemoteParams): string {
150
+ const { api } = this.config!;
151
+ let url = api.read || '';
152
+
153
+ // Build query params
154
+ const queryParams = new URLSearchParams();
155
+
156
+ if (params.page != null) {
157
+ queryParams.set('page', String(params.page));
158
+ }
159
+
160
+ if (params.pageSize != null) {
161
+ queryParams.set('pageSize', String(params.pageSize));
162
+ }
163
+
164
+ // Sort params
165
+ if (params.sort) {
166
+ const sorter = Array.isArray(params.sort) ? params.sort[0] : params.sort;
167
+ if (sorter) {
168
+ queryParams.set('sortBy', sorter.property);
169
+ queryParams.set('sortDir', sorter.direction);
170
+ }
171
+ }
172
+
173
+ // Filter params
174
+ if (params.filters && params.filters.length > 0) {
175
+ queryParams.set('filters', JSON.stringify(params.filters));
176
+ }
177
+
178
+ const queryString = queryParams.toString();
179
+ if (queryString) {
180
+ url += (url.includes('?') ? '&' : '?') + queryString;
181
+ }
182
+
183
+ return url;
184
+ }
185
+
186
+ // ==========================================================
187
+ // Response Parsing
188
+ // ==========================================================
189
+
190
+ private _getByPath(obj: any, path: string): any {
191
+ if (!obj || !path) return obj;
192
+
193
+ const parts = path.split('.');
194
+ let value = obj;
195
+
196
+ for (const part of parts) {
197
+ if (value == null) return undefined;
198
+ value = value[part];
199
+ }
200
+
201
+ return value;
202
+ }
203
+
204
+ private _parseResponse(response: any): RemoteResult {
205
+ const { dataPath, countPath } = this.config!.source;
206
+
207
+ const data = this._getByPath(response, dataPath);
208
+ const total = this._getByPath(response, countPath);
209
+
210
+ return {
211
+ data: Array.isArray(data) ? data : [],
212
+ total: typeof total === 'number' ? total : (Array.isArray(data) ? data.length : 0)
213
+ };
214
+ }
215
+
216
+ // ==========================================================
217
+ // Data Loading
218
+ // ==========================================================
219
+
220
+ async load(params: Partial<RemoteParams> = {}): Promise<RemoteResult> {
221
+ if (!this.isEnabled()) {
222
+ console.warn('[EzGridRemote] Remote mode not configured');
223
+ return { data: [], total: 0 };
224
+ }
225
+
226
+ if (this._isLoading) {
227
+ console.warn('[EzGridRemote] Load already in progress');
228
+ return { data: [], total: 0 };
229
+ }
230
+
231
+ const controller = this.grid.controller;
232
+ const state = controller?.state;
233
+ const { listenTo } = this.config!;
234
+
235
+ // Build final params
236
+ let finalParams: RemoteParams = {
237
+ page: params.page ?? state?.page ?? 1,
238
+ pageSize: params.pageSize ?? state?.pageSize ?? 25,
239
+ sort: params.sort ?? controller?._lastSortSnapshot ?? null,
240
+ filters: params.filters ?? (controller?._lastFilterSnapshot as any[] | null) ?? null
241
+ };
242
+
243
+ // Hook: beforeLoad - can modify params
244
+ const beforeLoadFn = this._resolveCallback(listenTo.beforeLoad);
245
+ if (beforeLoadFn) {
246
+ const modifiedParams = beforeLoadFn(finalParams as any, { grid: this.grid as any, remote: this as any });
247
+ if (modifiedParams) {
248
+ finalParams = modifiedParams as RemoteParams;
249
+ }
250
+ }
251
+
252
+ this._isLoading = true;
253
+ controller?.setLoading?.(true);
254
+
255
+ try {
256
+ const url = this._buildReadUrl(finalParams);
257
+
258
+ // Use ez._api for the request
259
+ const response = await ez._api.request(url);
260
+
261
+ let result = this._parseResponse(response);
262
+
263
+ // Hook: afterLoad - can modify result or just observe
264
+ const afterLoadFn = this._resolveCallback(listenTo.afterLoad);
265
+ if (afterLoadFn) {
266
+ const ctx = {
267
+ grid: this.grid as any,
268
+ remote: this as any,
269
+ params: finalParams as any,
270
+ response
271
+ };
272
+ const modifiedResult = afterLoadFn(result as any, ctx);
273
+ if (modifiedResult) {
274
+ result = modifiedResult as RemoteResult;
275
+ }
276
+ }
277
+
278
+ return result;
279
+
280
+ } catch (error) {
281
+ console.error('[EzGridRemote] Load failed:', error);
282
+
283
+ // Hook: onError
284
+ const onErrorFn = this._resolveCallback(listenTo.onError);
285
+ if (onErrorFn) {
286
+ onErrorFn(error as Error, { grid: this.grid as any, remote: this as any, params: finalParams as any });
287
+ }
288
+
289
+ controller?.setError?.(error as Error);
290
+ return { data: [], total: 0 };
291
+
292
+ } finally {
293
+ this._isLoading = false;
294
+ controller?.setLoading?.(false);
295
+ }
296
+ }
297
+
298
+ // ==========================================================
299
+ // CRUD Operations (future)
300
+ // ==========================================================
301
+
302
+ async create(data: any): Promise<any> {
303
+ if (!this.config?.api?.create) {
304
+ throw new Error('Create endpoint not configured');
305
+ }
306
+
307
+ return ez._api.request(this.config.api.create, {
308
+ method: 'POST',
309
+ body: data
310
+ });
311
+ }
312
+
313
+ async update(id: string | number, data: any): Promise<any> {
314
+ if (!this.config?.api?.update) {
315
+ throw new Error('Update endpoint not configured');
316
+ }
317
+
318
+ const url = `${this.config.api.update}/${id}`;
319
+ return ez._api.request(url, {
320
+ method: 'PUT',
321
+ body: data
322
+ });
323
+ }
324
+
325
+ async delete(id: string | number): Promise<any> {
326
+ if (!this.config?.api?.delete) {
327
+ throw new Error('Delete endpoint not configured');
328
+ }
329
+
330
+ const url = `${this.config.api.delete}/${id}`;
331
+ return ez._api.request(url, {
332
+ method: 'DELETE'
333
+ });
334
+ }
335
+ }
@@ -0,0 +1,231 @@
1
+ // EzGrid/state/EzGridSelection.ts
2
+
3
+ import type {
4
+ SelectionMode,
5
+ SelectionConfig,
6
+ SelectionSnapshot,
7
+ RowData,
8
+ RowKeyFn,
9
+ RowCallback
10
+ } from '../types.js';
11
+
12
+ export interface EzGridSelectionRef {
13
+ config: {
14
+ onRowClick?: RowCallback | string;
15
+ onRowDoubleClick?: RowCallback | string;
16
+ rowKey?: string | RowKeyFn;
17
+ };
18
+ controller?: {
19
+ state?: {
20
+ data: RowData[];
21
+ };
22
+ };
23
+ _onSelectionChanged: () => void;
24
+ }
25
+
26
+ // Re-export types for backwards compatibility
27
+ export type { SelectionMode, SelectionConfig, SelectionSnapshot } from '../types.js';
28
+
29
+ export class EzGridSelection {
30
+ grid: EzGridSelectionRef;
31
+ enabled: boolean;
32
+ mode: SelectionMode;
33
+ toggle: boolean;
34
+ selected: Set<string>;
35
+ lastIndex: number | null;
36
+
37
+ constructor(grid: EzGridSelectionRef, config: boolean | SelectionConfig | null | undefined) {
38
+ this.grid = grid;
39
+ this.mode = 'single';
40
+ this.toggle = false;
41
+ this.selected = new Set();
42
+ this.lastIndex = null;
43
+
44
+ if (config === false) {
45
+ this.enabled = false;
46
+ return;
47
+ }
48
+
49
+ this.enabled = true;
50
+
51
+ const hasRowActions =
52
+ typeof grid.config.onRowClick === 'function' ||
53
+ typeof grid.config.onRowDoubleClick === 'function';
54
+
55
+ let mode: SelectionMode = 'single';
56
+ let toggle = false;
57
+ let selected = new Set<string>();
58
+
59
+ if (config === true || config == null) {
60
+ // defaults
61
+ } else if (typeof config === 'object') {
62
+ mode = config.mode === 'multi' ? 'multi' : 'single';
63
+ toggle = !!config.toggle;
64
+
65
+ if (Array.isArray(config.selected)) {
66
+ selected = new Set(config.selected.map(String));
67
+ }
68
+ }
69
+
70
+ if (mode === 'single' && toggle && hasRowActions) {
71
+ if (import.meta.env.DEV) {
72
+ console.warn(
73
+ '[EzGrid] selection.toggle=true was ignore due ' +
74
+ 'onRowClick/onRowDoubleClick. in single mode, the selection ' +
75
+ 'represent active context and cant be empty.'
76
+ );
77
+ }
78
+ toggle = false;
79
+ }
80
+
81
+ this.mode = mode;
82
+ this.toggle = toggle;
83
+ this.selected = selected;
84
+ this.lastIndex = null;
85
+ }
86
+
87
+ isEnabled(): boolean {
88
+ return this.enabled === true;
89
+ }
90
+
91
+ isSelected(row: any): boolean {
92
+ if (!this.enabled) return false;
93
+
94
+ const key = this._getRowKeyString(row);
95
+ if (key == null) return false;
96
+
97
+ return this.selected.has(key);
98
+ }
99
+
100
+ getSelection(): string[] {
101
+ if (!this.enabled) return [];
102
+ return Array.from(this.selected);
103
+ }
104
+
105
+ clear(): void {
106
+ if (!this.enabled) return;
107
+
108
+ this.selected.clear();
109
+ this.lastIndex = null;
110
+ this._changed();
111
+ }
112
+
113
+ toggleRow(row: any, index: number): void {
114
+ if (!this.enabled) return;
115
+
116
+ const key = this._getRowKeyString(row);
117
+ if (key == null) return;
118
+
119
+ // Single mode: always select, toggle only if enabled
120
+ if (this.mode !== 'multi') {
121
+ if (this.selected.has(key) && this.toggle) {
122
+ this.selected.delete(key);
123
+ } else {
124
+ this.selected.clear();
125
+ this.selected.add(key);
126
+ }
127
+ this.lastIndex = index;
128
+ this._changed();
129
+ return;
130
+ }
131
+
132
+ // Multi mode: always allow toggle (deselect on re-click)
133
+ if (this.selected.has(key)) {
134
+ this.selected.delete(key);
135
+ } else {
136
+ this.selected.add(key);
137
+ }
138
+
139
+ this.lastIndex = index;
140
+ this._changed();
141
+ }
142
+
143
+ selectRange(toIndex: number): void {
144
+ if (!this.enabled) return;
145
+ if (this.mode !== 'multi') return;
146
+ if (this.lastIndex == null) return;
147
+
148
+ const data = this.grid.controller?.state?.data || [];
149
+ const from = Math.min(this.lastIndex, toIndex);
150
+ const to = Math.max(this.lastIndex, toIndex);
151
+
152
+ for (let i = from; i <= to; i++) {
153
+ const row = data[i];
154
+ const key = this._getRowKeyString(row);
155
+ if (key != null) this.selected.add(key);
156
+ }
157
+
158
+ this._changed();
159
+ }
160
+
161
+ selectAll(): void {
162
+ if (!this.enabled) return;
163
+
164
+ const data = this.grid.controller?.state?.data || [];
165
+ this.selected.clear();
166
+
167
+ for (const row of data) {
168
+ const key = this._getRowKeyString(row);
169
+ if (key != null) this.selected.add(key);
170
+ }
171
+
172
+ this.lastIndex = null;
173
+ this._changed();
174
+ }
175
+
176
+ isAllSelected(): boolean {
177
+ const data = this.grid.controller?.state?.data || [];
178
+ if (!data.length) return false;
179
+
180
+ let count = 0;
181
+ for (const row of data) {
182
+ const key = this._getRowKey(row);
183
+ if (key != null) count++;
184
+ }
185
+
186
+ return this.selected.size === count;
187
+ }
188
+
189
+ snapshot(): SelectionSnapshot | null {
190
+ if (!this.enabled) return null;
191
+
192
+ return {
193
+ mode: this.mode,
194
+ selected: Array.from(this.selected),
195
+ lastIndex: this.lastIndex
196
+ };
197
+ }
198
+
199
+ restore(snapshot: SelectionSnapshot | null): void {
200
+ if (!this.enabled || !snapshot) return;
201
+
202
+ // WARNING: Selection mode MUST come from grid config, not from persistence.
203
+ // Restoring `mode` from snapshot can silently downgrade multi-selection
204
+ // into single-selection when statefulPersist is enabled.
205
+ // Only derived state is allowed to be restored here.
206
+ this.selected = new Set(
207
+ Array.isArray(snapshot.selected)
208
+ ? snapshot.selected.map(String)
209
+ : []
210
+ );
211
+
212
+ this.lastIndex =
213
+ typeof snapshot.lastIndex === 'number'
214
+ ? snapshot.lastIndex
215
+ : null;
216
+ }
217
+
218
+ private _changed(): void {
219
+ this.grid._onSelectionChanged();
220
+ }
221
+
222
+ private _getRowKey(row: any): string | number | null {
223
+ const key = this.grid.config.rowKey;
224
+ return typeof key === 'function' ? key(row) : row?.[key as string];
225
+ }
226
+
227
+ private _getRowKeyString(row: any): string | null {
228
+ const key = this._getRowKey(row);
229
+ return key == null ? null : String(key);
230
+ }
231
+ }