mtrl-addons 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +117 -0
- package/AI.md +241 -0
- package/build.js +148 -0
- package/bun.lock +792 -0
- package/index.ts +7 -0
- package/package.json +10 -17
- package/scripts/analyze-orphaned-functions.ts +387 -0
- package/src/components/index.ts +45 -0
- package/src/components/list/api.ts +314 -0
- package/src/components/list/config.ts +352 -0
- package/src/components/list/constants.ts +56 -0
- package/src/components/list/features/api.ts +428 -0
- package/src/components/list/features/index.ts +31 -0
- package/src/components/list/features/list-manager.ts +502 -0
- package/src/components/list/features.ts +112 -0
- package/src/components/list/index.ts +39 -0
- package/src/components/list/list.ts +234 -0
- package/src/components/list/types.ts +513 -0
- package/src/core/collection/base-collection.ts +100 -0
- package/src/core/collection/collection-composer.ts +178 -0
- package/src/core/collection/collection.ts +745 -0
- package/src/core/collection/constants.ts +172 -0
- package/src/core/collection/events.ts +428 -0
- package/src/core/collection/features/api/loading.ts +279 -0
- package/src/core/collection/features/operations/data-operations.ts +147 -0
- package/src/core/collection/index.ts +104 -0
- package/src/core/collection/state.ts +497 -0
- package/src/core/collection/types.ts +404 -0
- package/src/core/compose/features/collection.ts +119 -0
- package/src/core/compose/features/index.ts +39 -0
- package/src/core/compose/features/performance.ts +161 -0
- package/src/core/compose/features/selection.ts +213 -0
- package/src/core/compose/features/styling.ts +108 -0
- package/src/core/compose/index.ts +31 -0
- package/src/core/index.ts +167 -0
- package/src/core/layout/config.ts +102 -0
- package/src/core/layout/index.ts +168 -0
- package/src/core/layout/jsx.ts +174 -0
- package/src/core/layout/schema.ts +963 -0
- package/src/core/layout/types.ts +92 -0
- package/src/core/list-manager/api.ts +599 -0
- package/src/core/list-manager/config.ts +593 -0
- package/src/core/list-manager/constants.ts +268 -0
- package/src/core/list-manager/features/api.ts +58 -0
- package/src/core/list-manager/features/collection/collection.ts +705 -0
- package/src/core/list-manager/features/collection/index.ts +17 -0
- package/src/core/list-manager/features/viewport/constants.ts +42 -0
- package/src/core/list-manager/features/viewport/index.ts +16 -0
- package/src/core/list-manager/features/viewport/item-size.ts +274 -0
- package/src/core/list-manager/features/viewport/loading.ts +263 -0
- package/src/core/list-manager/features/viewport/placeholders.ts +281 -0
- package/src/core/list-manager/features/viewport/rendering.ts +575 -0
- package/src/core/list-manager/features/viewport/scrollbar.ts +495 -0
- package/src/core/list-manager/features/viewport/scrolling.ts +795 -0
- package/src/core/list-manager/features/viewport/template.ts +220 -0
- package/src/core/list-manager/features/viewport/viewport.ts +654 -0
- package/src/core/list-manager/features/viewport/virtual.ts +309 -0
- package/src/core/list-manager/index.ts +279 -0
- package/src/core/list-manager/list-manager.ts +206 -0
- package/src/core/list-manager/types.ts +439 -0
- package/src/core/list-manager/utils/calculations.ts +290 -0
- package/src/core/list-manager/utils/range-calculator.ts +349 -0
- package/src/core/list-manager/utils/speed-tracker.ts +273 -0
- package/src/index.ts +17 -0
- package/src/styles/components/_list.scss +244 -0
- package/src/styles/index.scss +12 -0
- package/src/types/mtrl.d.ts +6 -0
- package/test/benchmarks/layout/advanced.test.ts +656 -0
- package/test/benchmarks/layout/comparison.test.ts +519 -0
- package/test/benchmarks/layout/performance-comparison.test.ts +274 -0
- package/test/benchmarks/layout/real-components.test.ts +733 -0
- package/test/benchmarks/layout/simple.test.ts +321 -0
- package/test/benchmarks/layout/stress.test.ts +990 -0
- package/test/collection/basic.test.ts +304 -0
- package/test/components/list.test.ts +256 -0
- package/test/core/collection/collection.test.ts +394 -0
- package/test/core/collection/failed-ranges.test.ts +270 -0
- package/test/core/compose/features.test.ts +183 -0
- package/test/core/layout/layout.test.ts +201 -0
- package/test/core/list-manager/features/collection.test.ts +704 -0
- package/test/core/list-manager/features/viewport.test.ts +698 -0
- package/test/core/list-manager/list-manager.test.ts +593 -0
- package/test/core/list-manager/utils/calculations.test.ts +433 -0
- package/test/core/list-manager/utils/range-calculator.test.ts +569 -0
- package/test/core/list-manager/utils/speed-tracker.test.ts +530 -0
- package/test/utils/dom-helpers.ts +275 -0
- package/test/utils/performance-helpers.ts +392 -0
- package/tsconfig.build.json +23 -0
- package/tsconfig.json +20 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -38
- package/dist/index.mjs +0 -8
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { ListComponent, ListConfig, ListAPI } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API configuration interface
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiConfig<T = any> {
|
|
7
|
+
component: ListComponent<T>;
|
|
8
|
+
config: ListConfig<T>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the public API layer for the List component
|
|
13
|
+
* Following mtrl's withAPI pattern
|
|
14
|
+
*/
|
|
15
|
+
export const withAPI =
|
|
16
|
+
<T = any>(apiConfig: ApiConfig<T>) =>
|
|
17
|
+
<C extends Partial<ListComponent<T>>>(component: C): C & ListAPI<T> => {
|
|
18
|
+
const { config } = apiConfig;
|
|
19
|
+
|
|
20
|
+
// Store references to orchestration methods before API overwrites them
|
|
21
|
+
const orchestrationMethods = {
|
|
22
|
+
scrollToIndex: component.scrollToIndex,
|
|
23
|
+
scrollToTop: component.scrollToTop,
|
|
24
|
+
scrollToBottom: component.scrollToBottom,
|
|
25
|
+
loadData: component.loadData,
|
|
26
|
+
reload: component.reload,
|
|
27
|
+
clear: component.clear,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Create clean public API interface
|
|
31
|
+
const api: ListAPI<T> = {
|
|
32
|
+
// Data management API
|
|
33
|
+
async loadData(): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
if (orchestrationMethods.loadData) {
|
|
36
|
+
await orchestrationMethods.loadData();
|
|
37
|
+
} else {
|
|
38
|
+
await component.collection?.loadPage?.(1);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("List: Failed to load data", error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async reload(): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
if (orchestrationMethods.reload) {
|
|
49
|
+
await orchestrationMethods.reload();
|
|
50
|
+
} else {
|
|
51
|
+
await component.collection?.refresh?.();
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("List: Failed to reload data", error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
clear(): void {
|
|
60
|
+
if (orchestrationMethods.clear) {
|
|
61
|
+
orchestrationMethods.clear();
|
|
62
|
+
} else {
|
|
63
|
+
component.collection?.clearItems?.();
|
|
64
|
+
component.clearSelection?.();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
addItems(items: T[], position: "start" | "end" = "end"): void {
|
|
69
|
+
if (!Array.isArray(items)) {
|
|
70
|
+
throw new Error("Items must be an array");
|
|
71
|
+
}
|
|
72
|
+
component.addItems?.(items, position);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
removeItems(indices: number[]): void {
|
|
76
|
+
if (!Array.isArray(indices)) {
|
|
77
|
+
throw new Error("Indices must be an array");
|
|
78
|
+
}
|
|
79
|
+
component.removeItems?.(indices);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
updateItem(index: number, item: T): void {
|
|
83
|
+
if (typeof index !== "number" || index < 0) {
|
|
84
|
+
throw new Error("Index must be a non-negative number");
|
|
85
|
+
}
|
|
86
|
+
component.updateItem?.(index, item);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
getItem(index: number): T | undefined {
|
|
90
|
+
if (typeof index !== "number") {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return component.getItem?.(index);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getItems(): T[] {
|
|
97
|
+
return component.getItems?.() || [];
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getItemCount(): number {
|
|
101
|
+
return component.collection?.getSize?.() || 0;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Scrolling API
|
|
105
|
+
async scrollToIndex(
|
|
106
|
+
index: number,
|
|
107
|
+
alignment: "start" | "center" | "end" = "start"
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (typeof index !== "number" || index < 0) {
|
|
110
|
+
throw new Error("Index must be a non-negative number");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// For virtual scrolling with infinite data, don't do strict bounds checking
|
|
114
|
+
// The orchestration layer will handle loading data as needed
|
|
115
|
+
try {
|
|
116
|
+
if (orchestrationMethods.scrollToIndex) {
|
|
117
|
+
await orchestrationMethods.scrollToIndex(index, alignment);
|
|
118
|
+
} else {
|
|
119
|
+
console.warn("scrollToIndex not available");
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("List: Failed to scroll to index", error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async scrollToTop(): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
if (orchestrationMethods.scrollToTop) {
|
|
130
|
+
await orchestrationMethods.scrollToTop();
|
|
131
|
+
} else {
|
|
132
|
+
console.warn("scrollToTop not available");
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("List: Failed to scroll to top", error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async scrollToBottom(): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
if (orchestrationMethods.scrollToBottom) {
|
|
143
|
+
await orchestrationMethods.scrollToBottom();
|
|
144
|
+
} else {
|
|
145
|
+
console.warn("scrollToBottom not available");
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error("List: Failed to scroll to bottom", error);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
getScrollPosition(): number {
|
|
154
|
+
return component.getScrollPosition?.() || 0;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Selection API
|
|
158
|
+
selectItems(indices: number[]): void {
|
|
159
|
+
if (!config.selection?.enabled) {
|
|
160
|
+
console.warn("List: Selection is not enabled");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!Array.isArray(indices)) {
|
|
165
|
+
throw new Error("Indices must be an array");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const itemCount = this.getItemCount();
|
|
169
|
+
const validIndices = indices.filter(
|
|
170
|
+
(index) =>
|
|
171
|
+
typeof index === "number" && index >= 0 && index < itemCount
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (config.selection.mode === "single" && validIndices.length > 1) {
|
|
175
|
+
console.warn("List: Single selection mode allows only one item");
|
|
176
|
+
component.selectItems?.([validIndices[0]]);
|
|
177
|
+
} else {
|
|
178
|
+
component.selectItems?.(validIndices);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
deselectItems(indices: number[]): void {
|
|
183
|
+
if (!config.selection?.enabled) {
|
|
184
|
+
console.warn("List: Selection is not enabled");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!Array.isArray(indices)) {
|
|
189
|
+
throw new Error("Indices must be an array");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
component.deselectItems?.(indices);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
clearSelection(): void {
|
|
196
|
+
if (!config.selection?.enabled) {
|
|
197
|
+
console.warn("List: Selection is not enabled");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
component.clearSelection?.();
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
getSelectedItems(): T[] {
|
|
205
|
+
return component.getSelectedItems?.() || [];
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
getSelectedIndices(): number[] {
|
|
209
|
+
return component.getSelectedIndices?.() || [];
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
isSelected(index: number): boolean {
|
|
213
|
+
if (typeof index !== "number") {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return component.isSelected?.(index) || false;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// State API
|
|
220
|
+
getState() {
|
|
221
|
+
return (
|
|
222
|
+
component.getState?.() || {
|
|
223
|
+
isLoading: false,
|
|
224
|
+
error: null,
|
|
225
|
+
isEmpty: true,
|
|
226
|
+
scrollTop: 0,
|
|
227
|
+
visibleRange: { start: 0, end: 0, count: 0 },
|
|
228
|
+
renderRange: { start: 0, end: 0, count: 0 },
|
|
229
|
+
selectedIndices: [],
|
|
230
|
+
totalItems: 0,
|
|
231
|
+
isVirtual: false,
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
isLoading(): boolean {
|
|
237
|
+
return component.isLoading?.() || false;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
hasError(): boolean {
|
|
241
|
+
return component.hasError?.() || false;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
isEmpty(): boolean {
|
|
245
|
+
return component.isEmpty?.() || true;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Rendering API
|
|
249
|
+
render(): void {
|
|
250
|
+
component.render?.();
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
updateViewport(): void {
|
|
254
|
+
component.updateViewport?.();
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
getVisibleRange() {
|
|
258
|
+
return component.getVisibleRange?.() || { start: 0, end: 0, count: 0 };
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
getRenderRange() {
|
|
262
|
+
return component.getRenderRange?.() || { start: 0, end: 0, count: 0 };
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// Template API
|
|
266
|
+
setTemplate(template) {
|
|
267
|
+
if (typeof template !== "function") {
|
|
268
|
+
throw new Error("Template must be a function");
|
|
269
|
+
}
|
|
270
|
+
component.setTemplate?.(template);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
setLoadingTemplate(template) {
|
|
274
|
+
// Store in config for future use
|
|
275
|
+
config.loadingTemplate = template;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
setEmptyTemplate(template) {
|
|
279
|
+
// Store in config for future use
|
|
280
|
+
config.emptyTemplate = template;
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
setErrorTemplate(template) {
|
|
284
|
+
// Store in config for future use
|
|
285
|
+
config.errorTemplate = template;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Configuration API
|
|
289
|
+
updateConfig(newConfig: Partial<ListConfig<T>>): void {
|
|
290
|
+
if (typeof newConfig !== "object" || newConfig === null) {
|
|
291
|
+
throw new Error("Config must be an object");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
component.updateConfig?.(newConfig);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
getConfig(): ListConfig<T> {
|
|
298
|
+
return component.getConfig?.() || config;
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Return component with API methods
|
|
303
|
+
return Object.assign(component, api) as C & ListAPI<T>;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Helper to get API configuration
|
|
308
|
+
*/
|
|
309
|
+
export const getApiConfig = <T = any>(
|
|
310
|
+
component: ListComponent<T>
|
|
311
|
+
): ApiConfig<T> => ({
|
|
312
|
+
component,
|
|
313
|
+
config: component.config,
|
|
314
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mtrl-addons List Component Configuration
|
|
3
|
+
*
|
|
4
|
+
* Configuration utilities and defaults for the list component.
|
|
5
|
+
* Follows mtrl patterns for component configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createComponentConfig,
|
|
10
|
+
createElementConfig as coreCreateElementConfig,
|
|
11
|
+
} from "mtrl/src/core/config/component";
|
|
12
|
+
import { LIST_DEFAULTS, LIST_CLASSES } from "./constants";
|
|
13
|
+
import type { ListConfig, ListItem } from "./types";
|
|
14
|
+
import type { ListComponent } from "./types";
|
|
15
|
+
import { DATA_PAGINATION } from "../../core/collection/constants";
|
|
16
|
+
import { VIRTUAL_SCROLLING } from "../../core/list-manager/constants";
|
|
17
|
+
import {
|
|
18
|
+
convertRenderItemToTemplate,
|
|
19
|
+
getDefaultTemplate,
|
|
20
|
+
} from "../../core/list-manager/features/viewport/template";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default configuration for the mtrl-addons List component
|
|
24
|
+
*/
|
|
25
|
+
export const defaultConfig: Partial<ListConfig> = {
|
|
26
|
+
// Collection settings (for API-connected lists)
|
|
27
|
+
adapter: undefined,
|
|
28
|
+
|
|
29
|
+
// Static data (for in-memory lists)
|
|
30
|
+
items: [],
|
|
31
|
+
|
|
32
|
+
// Template settings
|
|
33
|
+
template: undefined, // Will use default template if not provided
|
|
34
|
+
|
|
35
|
+
// Selection settings
|
|
36
|
+
selection: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
mode: "none",
|
|
39
|
+
selectedIndices: [],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Scroll settings
|
|
43
|
+
scroll: {
|
|
44
|
+
virtual: true,
|
|
45
|
+
itemSize: "auto",
|
|
46
|
+
estimatedItemSize: 50,
|
|
47
|
+
overscan: 5,
|
|
48
|
+
animation: false,
|
|
49
|
+
restorePosition: false,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Component settings
|
|
53
|
+
className: LIST_CLASSES.BASE,
|
|
54
|
+
prefix: "mtrl",
|
|
55
|
+
componentName: "list",
|
|
56
|
+
ariaLabel: "List",
|
|
57
|
+
debug: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates the base configuration for List component
|
|
62
|
+
* @param {ListConfig} config - User provided configuration
|
|
63
|
+
* @returns {ListConfig} Complete configuration with defaults applied
|
|
64
|
+
*/
|
|
65
|
+
export function createBaseConfig<T extends ListItem = ListItem>(
|
|
66
|
+
config: ListConfig<T> = {}
|
|
67
|
+
): Required<ListConfig<T>> {
|
|
68
|
+
console.log("🔧 [MTRL-ADDONS-LIST] Creating base configuration");
|
|
69
|
+
|
|
70
|
+
// Validate required configuration
|
|
71
|
+
if (!config.items && !config.adapter) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"List requires either static items or an adapter for data loading"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use mtrl core config system for proper merging and validation
|
|
78
|
+
const mergedConfig = createComponentConfig(defaultConfig, config, "list");
|
|
79
|
+
|
|
80
|
+
// Convert renderItem object to template function if provided
|
|
81
|
+
if (
|
|
82
|
+
(config as any).renderItem &&
|
|
83
|
+
typeof (config as any).renderItem === "object" &&
|
|
84
|
+
!mergedConfig.template
|
|
85
|
+
) {
|
|
86
|
+
mergedConfig.template = convertRenderItemToTemplate(
|
|
87
|
+
(config as any).renderItem
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validate selection configuration
|
|
92
|
+
if (
|
|
93
|
+
mergedConfig.selection?.enabled &&
|
|
94
|
+
mergedConfig.selection?.mode === undefined
|
|
95
|
+
) {
|
|
96
|
+
mergedConfig.selection.mode = "single";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return mergedConfig as Required<ListConfig<T>>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates element configuration for withElement
|
|
104
|
+
* @param {ListConfig} config - List configuration
|
|
105
|
+
* @returns {Object} Element configuration
|
|
106
|
+
*/
|
|
107
|
+
export function getElementConfig<T extends ListItem = ListItem>(
|
|
108
|
+
config: ListConfig<T>
|
|
109
|
+
): any {
|
|
110
|
+
const attributes = {
|
|
111
|
+
"data-component": "list",
|
|
112
|
+
"data-addons": "true",
|
|
113
|
+
role: "list",
|
|
114
|
+
"aria-label": config.ariaLabel || "List",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Create element config using mtrl core system
|
|
118
|
+
return coreCreateElementConfig(config, {
|
|
119
|
+
tag: "div",
|
|
120
|
+
attributes,
|
|
121
|
+
className: [LIST_CLASSES.BASE, LIST_CLASSES.ADDONS], // Clean: list list-addons
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates API configuration for the List component
|
|
127
|
+
* @param {Object} component - Component with list functionality
|
|
128
|
+
* @returns {Object} API configuration object for withApi
|
|
129
|
+
*/
|
|
130
|
+
export function getApiConfig<T extends ListItem = ListItem>(component: any) {
|
|
131
|
+
return {
|
|
132
|
+
// Data operations
|
|
133
|
+
data: {
|
|
134
|
+
add: component.add,
|
|
135
|
+
update: component.update,
|
|
136
|
+
remove: component.remove,
|
|
137
|
+
clear: component.clear,
|
|
138
|
+
refresh: component.refresh,
|
|
139
|
+
getItems: component.getItems,
|
|
140
|
+
getItem: component.getItem,
|
|
141
|
+
query: component.query,
|
|
142
|
+
sort: component.sort,
|
|
143
|
+
getSize: component.getSize,
|
|
144
|
+
isEmpty: component.isEmpty,
|
|
145
|
+
isLoading: component.isLoading,
|
|
146
|
+
getError: component.getError,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Selection operations (if enabled)
|
|
150
|
+
selection: component.config?.selection?.enabled
|
|
151
|
+
? {
|
|
152
|
+
selectItem: component.selectItem,
|
|
153
|
+
deselectItem: component.deselectItem,
|
|
154
|
+
selectAll: component.selectAll,
|
|
155
|
+
deselectAll: component.deselectAll,
|
|
156
|
+
getSelectedItems: component.getSelectedItems,
|
|
157
|
+
getSelectedIds: component.getSelectedIds,
|
|
158
|
+
}
|
|
159
|
+
: undefined,
|
|
160
|
+
|
|
161
|
+
// Scrolling operations
|
|
162
|
+
scrolling: {
|
|
163
|
+
scrollToItem: component.scrollToItem,
|
|
164
|
+
scrollToIndex: component.scrollToIndex,
|
|
165
|
+
scrollToPage: component.scrollToPage,
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Performance operations
|
|
169
|
+
performance: {
|
|
170
|
+
getMetrics: component.getMetrics,
|
|
171
|
+
resetMetrics: component.resetMetrics,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Template operations
|
|
175
|
+
template: {
|
|
176
|
+
setTemplate: component.setTemplate,
|
|
177
|
+
getTemplate: component.getTemplate,
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Events system
|
|
181
|
+
events: {
|
|
182
|
+
on: component.on,
|
|
183
|
+
off: component.off,
|
|
184
|
+
emit: component.emit,
|
|
185
|
+
subscribe: component.subscribe,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Lifecycle operations
|
|
189
|
+
lifecycle: {
|
|
190
|
+
destroy: component.destroy,
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Configuration access
|
|
194
|
+
config: {
|
|
195
|
+
selection: component.config?.selection,
|
|
196
|
+
scroll: component.config?.scroll,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validates list configuration
|
|
203
|
+
*/
|
|
204
|
+
export const validateConfig = (config: ListConfig): void => {
|
|
205
|
+
// Validate container
|
|
206
|
+
if (config.container) {
|
|
207
|
+
if (typeof config.container === "string") {
|
|
208
|
+
const element = document.querySelector(config.container);
|
|
209
|
+
if (!element) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`List container element not found: ${config.container}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
} else if (!(config.container instanceof HTMLElement)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"List container must be an HTMLElement or CSS selector string"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Validate template
|
|
222
|
+
if (config.template && typeof config.template !== "function") {
|
|
223
|
+
throw new Error("List template must be a function");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate items
|
|
227
|
+
if (config.items && !Array.isArray(config.items)) {
|
|
228
|
+
throw new Error("List items must be an array");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate selection mode
|
|
232
|
+
if (
|
|
233
|
+
config.selection?.mode &&
|
|
234
|
+
!["single", "multiple", "none"].includes(config.selection.mode)
|
|
235
|
+
) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
'List selection mode must be "single", "multiple", or "none"'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Validate scroll configuration
|
|
242
|
+
if (
|
|
243
|
+
config.scroll?.itemSize !== "auto" &&
|
|
244
|
+
typeof config.scroll?.itemSize === "number" &&
|
|
245
|
+
config.scroll.itemSize <= 0
|
|
246
|
+
) {
|
|
247
|
+
throw new Error('List item size must be positive number or "auto"');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Validate density
|
|
251
|
+
if (
|
|
252
|
+
config.density &&
|
|
253
|
+
!["default", "compact", "comfortable"].includes(config.density)
|
|
254
|
+
) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
'List density must be "default", "compact", or "comfortable"'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Validate variant
|
|
261
|
+
if (
|
|
262
|
+
config.variant &&
|
|
263
|
+
!["default", "dense", "comfortable"].includes(config.variant)
|
|
264
|
+
) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'List variant must be "default", "dense", or "comfortable"'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Creates Collection configuration from List config
|
|
273
|
+
*/
|
|
274
|
+
export const getCollectionConfig = (config: ListConfig) => ({
|
|
275
|
+
adapter: config.adapter,
|
|
276
|
+
items: config.items,
|
|
277
|
+
pageSize: config.collection?.limit || DATA_PAGINATION.DEFAULT_PAGE_SIZE,
|
|
278
|
+
cache: config.collection?.cache || {
|
|
279
|
+
enabled: true,
|
|
280
|
+
maxSize: 1000,
|
|
281
|
+
ttl: 300000, // 5 minutes
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Creates List Manager configuration from List config
|
|
287
|
+
*/
|
|
288
|
+
export const getListManagerConfig = (config: ListConfig) => ({
|
|
289
|
+
// Container
|
|
290
|
+
container: config.container,
|
|
291
|
+
|
|
292
|
+
// Collection configuration
|
|
293
|
+
collection: getCollectionConfig(config),
|
|
294
|
+
|
|
295
|
+
// Template configuration
|
|
296
|
+
template: {
|
|
297
|
+
template: config.template,
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// Component prefix
|
|
301
|
+
prefix: config.prefix || "mtrl",
|
|
302
|
+
|
|
303
|
+
// Orientation configuration
|
|
304
|
+
orientation: {
|
|
305
|
+
orientation: config.orientation?.orientation || "vertical",
|
|
306
|
+
autoDetect: config.orientation?.autoDetect || false,
|
|
307
|
+
reverse: config.orientation?.reverse || false,
|
|
308
|
+
crossAxisAlignment: config.orientation?.crossAxisAlignment || "stretch",
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Virtual scrolling
|
|
312
|
+
virtual: {
|
|
313
|
+
enabled: config.scroll?.virtual ?? true,
|
|
314
|
+
itemSize: config.scroll?.itemSize ?? "auto",
|
|
315
|
+
estimatedItemSize: config.scroll?.estimatedItemSize ?? 50,
|
|
316
|
+
overscan: config.scroll?.overscan ?? VIRTUAL_SCROLLING.DEFAULT_OVERSCAN,
|
|
317
|
+
...config.listManager?.virtual,
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// Element recycling
|
|
321
|
+
recycling: {
|
|
322
|
+
enabled: config.listManager?.recycling?.enabled ?? true,
|
|
323
|
+
maxPoolSize: config.listManager?.recycling?.maxPoolSize ?? 100,
|
|
324
|
+
minPoolSize: config.listManager?.recycling?.minPoolSize ?? 10,
|
|
325
|
+
...config.listManager?.recycling,
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// Intersection observers
|
|
329
|
+
intersection: {
|
|
330
|
+
pagination: {
|
|
331
|
+
enabled: true,
|
|
332
|
+
rootMargin: "200px",
|
|
333
|
+
threshold: 0.1,
|
|
334
|
+
...config.listManager?.intersection?.pagination,
|
|
335
|
+
},
|
|
336
|
+
loading: {
|
|
337
|
+
enabled: true,
|
|
338
|
+
...config.listManager?.intersection?.loading,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Performance configuration
|
|
343
|
+
performance: {
|
|
344
|
+
frameScheduling: true,
|
|
345
|
+
memoryCleanup: true,
|
|
346
|
+
...config.listManager?.performance,
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// Debug and styling
|
|
350
|
+
debug: config.debug || false,
|
|
351
|
+
componentName: "list-manager",
|
|
352
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mtrl-addons List Component Constants
|
|
3
|
+
*
|
|
4
|
+
* Constants for the addons list component.
|
|
5
|
+
* Note: Base mtrl list functionality is handled by mtrl core.
|
|
6
|
+
* These constants are for addons-specific features only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CSS class names for List component
|
|
11
|
+
* Following BEM convention: component__element--modifier
|
|
12
|
+
* Note: mtrl prefix is added automatically by core DOM classes system
|
|
13
|
+
*/
|
|
14
|
+
export const LIST_CLASSES = {
|
|
15
|
+
BASE: "list",
|
|
16
|
+
ADDONS: "list-addons", // Addons-specific class
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default values for List component configuration
|
|
21
|
+
* These supplement mtrl's base list defaults with addons-specific values
|
|
22
|
+
*/
|
|
23
|
+
export const LIST_DEFAULTS = {
|
|
24
|
+
// Collection defaults (addons-specific)
|
|
25
|
+
INITIAL_CAPACITY: 100,
|
|
26
|
+
RENDER_BUFFER_SIZE: 10,
|
|
27
|
+
RENDER_DEBOUNCE: 16, // ~1 frame at 60fps
|
|
28
|
+
|
|
29
|
+
// Template defaults (addons-specific)
|
|
30
|
+
TEMPLATE_ENGINE: "object" as const,
|
|
31
|
+
|
|
32
|
+
// Performance defaults (addons-specific)
|
|
33
|
+
PERFORMANCE_TRACKING: true,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Event names for List component
|
|
38
|
+
* Following mtrl event naming conventions
|
|
39
|
+
*/
|
|
40
|
+
export const LIST_EVENTS = {
|
|
41
|
+
// Core events (inherited from mtrl base)
|
|
42
|
+
CREATED: "list:created",
|
|
43
|
+
DESTROYED: "list:destroyed",
|
|
44
|
+
|
|
45
|
+
// Data events (addons-specific)
|
|
46
|
+
DATA_LOADED: "list:data-loaded",
|
|
47
|
+
DATA_ERROR: "list:data-error",
|
|
48
|
+
DATA_UPDATED: "list:data-updated",
|
|
49
|
+
|
|
50
|
+
// Selection events (addons-specific enhancements)
|
|
51
|
+
SELECTION_CHANGED: "list:selection-changed",
|
|
52
|
+
SELECTION_CLEARED: "list:selection-cleared",
|
|
53
|
+
|
|
54
|
+
// Performance events (addons-specific)
|
|
55
|
+
PERFORMANCE_METRICS: "list:performance-metrics",
|
|
56
|
+
} as const;
|