mtrl-addons 0.2.1 → 0.2.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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +286 -110
- package/package.json +2 -1
- package/src/components/vlist/features/api.ts +316 -12
- package/src/components/vlist/features/selection.ts +248 -256
- package/src/components/vlist/features/viewport.ts +1 -7
- package/src/components/vlist/index.ts +1 -0
- package/src/components/vlist/types.ts +140 -8
- package/src/core/layout/schema.ts +81 -38
- package/src/core/viewport/constants.ts +7 -2
- package/src/core/viewport/features/collection.ts +376 -76
- package/src/core/viewport/features/item-size.ts +4 -4
- package/src/core/viewport/features/momentum.ts +11 -2
- package/src/core/viewport/features/rendering.ts +424 -30
- package/src/core/viewport/features/scrolling.ts +41 -25
- package/src/core/viewport/features/utils.ts +11 -5
- package/src/core/viewport/features/virtual.ts +169 -28
- package/src/core/viewport/types.ts +2 -2
- package/src/core/viewport/viewport.ts +29 -10
- package/src/styles/components/_vlist.scss +234 -213
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { BaseComponent, ElementComponent } from "mtrl";
|
|
6
|
+
|
|
7
|
+
/** Options for removeItemById */
|
|
8
|
+
export interface RemoveItemOptions {
|
|
9
|
+
/** Track as pending removal to filter from future fetches (default: true) */
|
|
10
|
+
trackPending?: boolean;
|
|
11
|
+
/** Timeout in ms to clear pending removal (default: 5000) */
|
|
12
|
+
pendingTimeout?: number;
|
|
13
|
+
}
|
|
6
14
|
// Collection types are not exposed by mtrl; define minimal interfaces locally
|
|
7
15
|
export interface CollectionItem {
|
|
8
16
|
id: string | number;
|
|
@@ -18,7 +26,7 @@ export interface CollectionConfig<T = any> {
|
|
|
18
26
|
export interface Collection<T = any> {
|
|
19
27
|
loadMissingRanges?: (
|
|
20
28
|
range: { start: number; end: number },
|
|
21
|
-
reason?: string
|
|
29
|
+
reason?: string,
|
|
22
30
|
) => Promise<void>;
|
|
23
31
|
}
|
|
24
32
|
import type { ViewportComponent } from "../../core/viewport/types";
|
|
@@ -82,7 +90,7 @@ export type ListTemplate = (item: any, index: number) => string | HTMLElement;
|
|
|
82
90
|
*/
|
|
83
91
|
export type ListItemTemplate<T = any> = (
|
|
84
92
|
item: T,
|
|
85
|
-
index: number
|
|
93
|
+
index: number,
|
|
86
94
|
) => string | HTMLElement;
|
|
87
95
|
|
|
88
96
|
/**
|
|
@@ -325,6 +333,19 @@ export interface ListEvents<T = any> {
|
|
|
325
333
|
"render:complete": {
|
|
326
334
|
renderRange: { start: number; end: number; count: number };
|
|
327
335
|
};
|
|
336
|
+
|
|
337
|
+
/** Item updated */
|
|
338
|
+
"item:updated": {
|
|
339
|
+
item: T;
|
|
340
|
+
index: number;
|
|
341
|
+
previousItem: T;
|
|
342
|
+
wasVisible: boolean;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** Viewport range loaded (data available for range) */
|
|
346
|
+
"viewport:range-loaded": {
|
|
347
|
+
range: { start: number; end: number };
|
|
348
|
+
};
|
|
328
349
|
}
|
|
329
350
|
|
|
330
351
|
/**
|
|
@@ -347,9 +368,80 @@ export interface ListAPI<T extends ListItem = ListItem> {
|
|
|
347
368
|
/** Remove items by indices */
|
|
348
369
|
removeItems(indices: number[]): void;
|
|
349
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Remove item at a specific index
|
|
373
|
+
* Removes the item from the collection, updates totalItems, and triggers re-render
|
|
374
|
+
* @param index - The index of the item to remove
|
|
375
|
+
* @returns true if item was found and removed, false otherwise
|
|
376
|
+
*/
|
|
377
|
+
removeItem(index: number): boolean;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Remove item by ID
|
|
381
|
+
* Finds the item in the collection by its ID and removes it
|
|
382
|
+
* Updates totalItems and triggers re-render of visible items
|
|
383
|
+
* Optionally tracks as pending removal to filter from future fetches
|
|
384
|
+
* @param id - The item ID to find and remove
|
|
385
|
+
* @param options - Remove options (trackPending, pendingTimeout)
|
|
386
|
+
* @returns true if item was found and removed, false otherwise
|
|
387
|
+
*/
|
|
388
|
+
removeItemById(id: string | number, options?: RemoveItemOptions): boolean;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if an item ID is pending removal
|
|
392
|
+
* @param id - The item ID to check
|
|
393
|
+
* @returns true if the item is pending removal
|
|
394
|
+
*/
|
|
395
|
+
isPendingRemoval(id: string | number): boolean;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get all pending removal IDs
|
|
399
|
+
* @returns Set of pending removal IDs
|
|
400
|
+
*/
|
|
401
|
+
getPendingRemovals(): Set<string | number>;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Clear a specific pending removal
|
|
405
|
+
* @param id - The item ID to clear from pending removals
|
|
406
|
+
*/
|
|
407
|
+
clearPendingRemoval(id: string | number): void;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Clear all pending removals
|
|
411
|
+
*/
|
|
412
|
+
clearAllPendingRemovals(): void;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Filter items array to exclude pending removals
|
|
416
|
+
* Utility method for use in collection adapters
|
|
417
|
+
* @param items - Array of items to filter
|
|
418
|
+
* @returns Filtered array without pending removal items
|
|
419
|
+
*/
|
|
420
|
+
filterPendingRemovals<I extends { id?: any; _id?: any }>(items: I[]): I[];
|
|
421
|
+
|
|
350
422
|
/** Update item at index */
|
|
351
423
|
updateItem(index: number, item: T): void;
|
|
352
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Update item by ID
|
|
427
|
+
* Finds the item in the collection by its ID and updates it with new data
|
|
428
|
+
* Re-renders the item if currently visible in the viewport
|
|
429
|
+
* @param id - The item ID to find
|
|
430
|
+
* @param data - Partial data to merge with existing item, or full item replacement
|
|
431
|
+
* @param options - Update options
|
|
432
|
+
* @returns true if item was found and updated, false otherwise
|
|
433
|
+
*/
|
|
434
|
+
updateItemById(
|
|
435
|
+
id: string | number,
|
|
436
|
+
data: Partial<T>,
|
|
437
|
+
options?: {
|
|
438
|
+
/** If true, replace the entire item instead of merging (default: false) */
|
|
439
|
+
replace?: boolean;
|
|
440
|
+
/** If true, re-render even if not visible (default: false) */
|
|
441
|
+
forceRender?: boolean;
|
|
442
|
+
},
|
|
443
|
+
): boolean;
|
|
444
|
+
|
|
353
445
|
/** Get item at index */
|
|
354
446
|
getItem(index: number): T | undefined;
|
|
355
447
|
|
|
@@ -363,7 +455,7 @@ export interface ListAPI<T extends ListItem = ListItem> {
|
|
|
363
455
|
/** Scroll to item index */
|
|
364
456
|
scrollToIndex(
|
|
365
457
|
index: number,
|
|
366
|
-
alignment?: "start" | "center" | "end"
|
|
458
|
+
alignment?: "start" | "center" | "end",
|
|
367
459
|
): Promise<void>;
|
|
368
460
|
|
|
369
461
|
/** Scroll to top */
|
|
@@ -404,6 +496,32 @@ export interface ListAPI<T extends ListItem = ListItem> {
|
|
|
404
496
|
/** Check if item is selected */
|
|
405
497
|
isSelected(index: number): boolean;
|
|
406
498
|
|
|
499
|
+
/** Select an item by its ID
|
|
500
|
+
* @param id - The ID of the item to select
|
|
501
|
+
* @param silent - If true, selection won't emit change event (default: false)
|
|
502
|
+
*/
|
|
503
|
+
selectById(id: string | number, silent?: boolean): boolean;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Select item at index, scrolling and waiting for data if needed
|
|
507
|
+
* Handles virtual scrolling by loading data before selecting
|
|
508
|
+
*/
|
|
509
|
+
selectAtIndex(index: number): Promise<boolean>;
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Select next item relative to current selection
|
|
513
|
+
* Handles virtual scrolling by loading data before selecting
|
|
514
|
+
* @returns Promise resolving to true if selection changed, false if at end
|
|
515
|
+
*/
|
|
516
|
+
selectNext(): Promise<boolean>;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Select previous item relative to current selection
|
|
520
|
+
* Handles virtual scrolling by loading data before selecting
|
|
521
|
+
* @returns Promise resolving to true if selection changed, false if at start
|
|
522
|
+
*/
|
|
523
|
+
selectPrevious(): Promise<boolean>;
|
|
524
|
+
|
|
407
525
|
// State
|
|
408
526
|
/** Get current list state */
|
|
409
527
|
getState(): ListState;
|
|
@@ -442,7 +560,7 @@ export interface ListAPI<T extends ListItem = ListItem> {
|
|
|
442
560
|
|
|
443
561
|
/** Set error template */
|
|
444
562
|
setErrorTemplate(
|
|
445
|
-
template: string | ((error: Error) => string | HTMLElement)
|
|
563
|
+
template: string | ((error: Error) => string | HTMLElement),
|
|
446
564
|
): void;
|
|
447
565
|
|
|
448
566
|
// Configuration
|
|
@@ -482,15 +600,15 @@ export interface ListComponent<T extends ListItem = ListItem>
|
|
|
482
600
|
/** Event system (inherited from BaseComponent) */
|
|
483
601
|
on<K extends keyof ListEvents<T>>(
|
|
484
602
|
event: K,
|
|
485
|
-
handler: (payload: ListEvents<T>[K]) => void
|
|
603
|
+
handler: (payload: ListEvents<T>[K]) => void,
|
|
486
604
|
): void;
|
|
487
605
|
emit<K extends keyof ListEvents<T>>(
|
|
488
606
|
event: K,
|
|
489
|
-
payload: ListEvents<T>[K]
|
|
607
|
+
payload: ListEvents<T>[K],
|
|
490
608
|
): void;
|
|
491
609
|
off<K extends keyof ListEvents<T>>(
|
|
492
610
|
event: K,
|
|
493
|
-
handler?: (payload: ListEvents<T>[K]) => void
|
|
611
|
+
handler?: (payload: ListEvents<T>[K]) => void,
|
|
494
612
|
): void;
|
|
495
613
|
}
|
|
496
614
|
|
|
@@ -532,13 +650,25 @@ export interface VListConfig<T extends ListItem = ListItem> {
|
|
|
532
650
|
ariaLabel?: string;
|
|
533
651
|
debug?: boolean;
|
|
534
652
|
|
|
653
|
+
// Initial scroll position (0-based index)
|
|
654
|
+
// When set, VList will start loading from this position instead of 0
|
|
655
|
+
initialScrollIndex?: number;
|
|
656
|
+
|
|
657
|
+
// ID of item to select after initial load completes
|
|
658
|
+
// Works with initialScrollIndex to scroll to position and then select the item
|
|
659
|
+
selectId?: string | number;
|
|
660
|
+
|
|
661
|
+
// Whether to automatically load data on initialization (default: true)
|
|
662
|
+
// Set to false to defer loading until manually triggered
|
|
663
|
+
autoLoad?: boolean;
|
|
664
|
+
|
|
535
665
|
// Data source
|
|
536
666
|
items?: T[];
|
|
537
667
|
|
|
538
668
|
// Template for rendering items
|
|
539
669
|
template?: (
|
|
540
670
|
item: T,
|
|
541
|
-
index: number
|
|
671
|
+
index: number,
|
|
542
672
|
) => string | HTMLElement | any[] | Record<string, any>;
|
|
543
673
|
|
|
544
674
|
// Collection configuration
|
|
@@ -572,6 +702,8 @@ export interface VListConfig<T extends ListItem = ListItem> {
|
|
|
572
702
|
bufferSize?: number;
|
|
573
703
|
renderDebounce?: number;
|
|
574
704
|
maxConcurrentRequests?: number;
|
|
705
|
+
/** Velocity threshold (px/ms) above which data loading is cancelled and placeholders are shown. Default: 2 */
|
|
706
|
+
cancelLoadThreshold?: number;
|
|
575
707
|
};
|
|
576
708
|
|
|
577
709
|
// Selection configuration
|
|
@@ -129,7 +129,7 @@ const PREFIX_WITH_DASH = `${PREFIX}-`;
|
|
|
129
129
|
function getCachedClassName(
|
|
130
130
|
type: string,
|
|
131
131
|
property: string,
|
|
132
|
-
value: string | number
|
|
132
|
+
value: string | number,
|
|
133
133
|
): string {
|
|
134
134
|
const key = `${type}-${property}-${value}`;
|
|
135
135
|
if (!classCache.has(key)) {
|
|
@@ -138,7 +138,7 @@ function getCachedClassName(
|
|
|
138
138
|
key,
|
|
139
139
|
property === ""
|
|
140
140
|
? `layout__item--${value}`
|
|
141
|
-
: `layout__item--${property}-${value}
|
|
141
|
+
: `layout__item--${property}-${value}`,
|
|
142
142
|
);
|
|
143
143
|
} else {
|
|
144
144
|
// For layout classes, align uses layout--{type}-{value}
|
|
@@ -184,7 +184,7 @@ function releaseFragment(fragment: DocumentFragment): void {
|
|
|
184
184
|
*/
|
|
185
185
|
function processClassNames(
|
|
186
186
|
options: Record<string, any>,
|
|
187
|
-
skipPrefix = false
|
|
187
|
+
skipPrefix = false,
|
|
188
188
|
): Record<string, any> {
|
|
189
189
|
if (!options) return options;
|
|
190
190
|
|
|
@@ -231,7 +231,7 @@ function processClassNames(
|
|
|
231
231
|
.split(/\s+/)
|
|
232
232
|
.filter(Boolean)
|
|
233
233
|
.map((cls) =>
|
|
234
|
-
cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls
|
|
234
|
+
cls.startsWith(PREFIX_WITH_DASH) ? cls : PREFIX_WITH_DASH + cls,
|
|
235
235
|
)
|
|
236
236
|
.join(" ");
|
|
237
237
|
}
|
|
@@ -271,7 +271,7 @@ interface ExtractedParameters {
|
|
|
271
271
|
function extractParameters(
|
|
272
272
|
schema: SchemaItem[],
|
|
273
273
|
startIndex: number,
|
|
274
|
-
defaultCreator: Function
|
|
274
|
+
defaultCreator: Function,
|
|
275
275
|
): ExtractedParameters {
|
|
276
276
|
const items = schema.slice(startIndex, startIndex + 3);
|
|
277
277
|
let creator, name, options;
|
|
@@ -379,7 +379,7 @@ function hasClass(element: HTMLElement, className: string): boolean {
|
|
|
379
379
|
*/
|
|
380
380
|
function createComponentInstance(
|
|
381
381
|
Component: any,
|
|
382
|
-
options: Record<string, any> = {}
|
|
382
|
+
options: Record<string, any> = {},
|
|
383
383
|
): any {
|
|
384
384
|
try {
|
|
385
385
|
// Destructure special configs in one operation
|
|
@@ -453,7 +453,7 @@ function createComponentInstance(
|
|
|
453
453
|
}
|
|
454
454
|
} else {
|
|
455
455
|
for (const [eventName, handler] of Object.entries(
|
|
456
|
-
finalEventsConfig
|
|
456
|
+
finalEventsConfig,
|
|
457
457
|
)) {
|
|
458
458
|
if (typeof handler === "function") {
|
|
459
459
|
element.addEventListener(eventName, handler);
|
|
@@ -481,7 +481,7 @@ function createComponentInstance(
|
|
|
481
481
|
*/
|
|
482
482
|
function applyLayoutClasses(
|
|
483
483
|
element: HTMLElement,
|
|
484
|
-
layoutConfig: LayoutConfig
|
|
484
|
+
layoutConfig: LayoutConfig,
|
|
485
485
|
): void {
|
|
486
486
|
if (!element || !layoutConfig) return;
|
|
487
487
|
|
|
@@ -497,21 +497,21 @@ function applyLayoutClasses(
|
|
|
497
497
|
addClass(
|
|
498
498
|
element,
|
|
499
499
|
PREFIX_WITH_DASH +
|
|
500
|
-
getCachedClassName(layoutType, "gap", layoutConfig.gap)
|
|
500
|
+
getCachedClassName(layoutType, "gap", layoutConfig.gap),
|
|
501
501
|
);
|
|
502
502
|
}
|
|
503
503
|
if (layoutConfig.align) {
|
|
504
504
|
addClass(
|
|
505
505
|
element,
|
|
506
506
|
PREFIX_WITH_DASH +
|
|
507
|
-
getCachedClassName(layoutType, "align", layoutConfig.align)
|
|
507
|
+
getCachedClassName(layoutType, "align", layoutConfig.align),
|
|
508
508
|
);
|
|
509
509
|
}
|
|
510
510
|
if (layoutConfig.justify) {
|
|
511
511
|
addClass(
|
|
512
512
|
element,
|
|
513
513
|
PREFIX_WITH_DASH +
|
|
514
|
-
getCachedClassName(layoutType, "justify", layoutConfig.justify)
|
|
514
|
+
getCachedClassName(layoutType, "justify", layoutConfig.justify),
|
|
515
515
|
);
|
|
516
516
|
}
|
|
517
517
|
}
|
|
@@ -522,17 +522,17 @@ function applyLayoutClasses(
|
|
|
522
522
|
addClass(
|
|
523
523
|
element,
|
|
524
524
|
PREFIX_WITH_DASH +
|
|
525
|
-
getCachedClassName("grid", "cols", layoutConfig.columns)
|
|
525
|
+
getCachedClassName("grid", "cols", layoutConfig.columns),
|
|
526
526
|
);
|
|
527
527
|
} else if (layoutConfig.columns === "auto-fill") {
|
|
528
528
|
addClass(
|
|
529
529
|
element,
|
|
530
|
-
PREFIX_WITH_DASH + getCachedClassName("grid", "fill", "auto")
|
|
530
|
+
PREFIX_WITH_DASH + getCachedClassName("grid", "fill", "auto"),
|
|
531
531
|
);
|
|
532
532
|
} else if (layoutConfig.columns === "auto-fit") {
|
|
533
533
|
addClass(
|
|
534
534
|
element,
|
|
535
|
-
PREFIX_WITH_DASH + getCachedClassName("grid", "cols", "auto-fit")
|
|
535
|
+
PREFIX_WITH_DASH + getCachedClassName("grid", "cols", "auto-fit"),
|
|
536
536
|
);
|
|
537
537
|
}
|
|
538
538
|
if (layoutConfig.dense)
|
|
@@ -569,7 +569,7 @@ function applyLayoutClasses(
|
|
|
569
569
|
*/
|
|
570
570
|
function applyLayoutItemClasses(
|
|
571
571
|
element: HTMLElement,
|
|
572
|
-
itemConfig: LayoutItemConfig
|
|
572
|
+
itemConfig: LayoutItemConfig,
|
|
573
573
|
): void {
|
|
574
574
|
if (!element || !itemConfig) return;
|
|
575
575
|
|
|
@@ -579,53 +579,53 @@ function applyLayoutItemClasses(
|
|
|
579
579
|
if (itemConfig.width && itemConfig.width >= 1 && itemConfig.width <= 12) {
|
|
580
580
|
addClass(
|
|
581
581
|
element,
|
|
582
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "", itemConfig.width)
|
|
582
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "", itemConfig.width),
|
|
583
583
|
);
|
|
584
584
|
}
|
|
585
585
|
if (itemConfig.sm)
|
|
586
586
|
addClass(
|
|
587
587
|
element,
|
|
588
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "sm", itemConfig.sm)
|
|
588
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "sm", itemConfig.sm),
|
|
589
589
|
);
|
|
590
590
|
if (itemConfig.md)
|
|
591
591
|
addClass(
|
|
592
592
|
element,
|
|
593
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "md", itemConfig.md)
|
|
593
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "md", itemConfig.md),
|
|
594
594
|
);
|
|
595
595
|
if (itemConfig.lg)
|
|
596
596
|
addClass(
|
|
597
597
|
element,
|
|
598
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "lg", itemConfig.lg)
|
|
598
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "lg", itemConfig.lg),
|
|
599
599
|
);
|
|
600
600
|
if (itemConfig.xl)
|
|
601
601
|
addClass(
|
|
602
602
|
element,
|
|
603
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "xl", itemConfig.xl)
|
|
603
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "xl", itemConfig.xl),
|
|
604
604
|
);
|
|
605
605
|
|
|
606
606
|
// Grid span classes
|
|
607
607
|
if (itemConfig.span)
|
|
608
608
|
addClass(
|
|
609
609
|
element,
|
|
610
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "span", itemConfig.span)
|
|
610
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "span", itemConfig.span),
|
|
611
611
|
);
|
|
612
612
|
if (itemConfig.rowSpan)
|
|
613
613
|
addClass(
|
|
614
614
|
element,
|
|
615
615
|
PREFIX_WITH_DASH +
|
|
616
|
-
getCachedClassName("item", "row-span", itemConfig.rowSpan)
|
|
616
|
+
getCachedClassName("item", "row-span", itemConfig.rowSpan),
|
|
617
617
|
);
|
|
618
618
|
|
|
619
619
|
// Order and alignment
|
|
620
620
|
if (itemConfig.order)
|
|
621
621
|
addClass(
|
|
622
622
|
element,
|
|
623
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "order", itemConfig.order)
|
|
623
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "order", itemConfig.order),
|
|
624
624
|
);
|
|
625
625
|
if (itemConfig.align)
|
|
626
626
|
addClass(
|
|
627
627
|
element,
|
|
628
|
-
PREFIX_WITH_DASH + getCachedClassName("item", "self", itemConfig.align)
|
|
628
|
+
PREFIX_WITH_DASH + getCachedClassName("item", "self", itemConfig.align),
|
|
629
629
|
);
|
|
630
630
|
if (itemConfig.auto)
|
|
631
631
|
addClass(element, `${PREFIX_WITH_DASH}layout__item--auto`);
|
|
@@ -638,10 +638,10 @@ function getLayoutType(element: HTMLElement): string {
|
|
|
638
638
|
return hasClass(element, `${PREFIX_WITH_DASH}layout--stack`)
|
|
639
639
|
? "stack"
|
|
640
640
|
: hasClass(element, `${PREFIX_WITH_DASH}layout--row`)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
641
|
+
? "row"
|
|
642
|
+
: hasClass(element, `${PREFIX_WITH_DASH}layout--grid`)
|
|
643
|
+
? "grid"
|
|
644
|
+
: "";
|
|
645
645
|
}
|
|
646
646
|
|
|
647
647
|
// ============================================================================
|
|
@@ -656,7 +656,7 @@ function processArraySchema(
|
|
|
656
656
|
schema: SchemaItem[] | any,
|
|
657
657
|
parentElement: HTMLElement | null = null,
|
|
658
658
|
level: number = 0,
|
|
659
|
-
options: LayoutOptions = {}
|
|
659
|
+
options: LayoutOptions = {},
|
|
660
660
|
): LayoutResult {
|
|
661
661
|
level++;
|
|
662
662
|
const layout: Record<string, any> = {};
|
|
@@ -678,6 +678,11 @@ function processArraySchema(
|
|
|
678
678
|
if (Array.isArray(item)) {
|
|
679
679
|
const container = component || parentElement;
|
|
680
680
|
const result = processArraySchema(item, container, level, options);
|
|
681
|
+
// Merge nested components array instead of overwriting
|
|
682
|
+
if (Array.isArray(result.layout.components)) {
|
|
683
|
+
components.push(...result.layout.components);
|
|
684
|
+
delete result.layout.components;
|
|
685
|
+
}
|
|
681
686
|
Object.assign(layout, result.layout);
|
|
682
687
|
continue;
|
|
683
688
|
}
|
|
@@ -774,7 +779,7 @@ function processArraySchema(
|
|
|
774
779
|
function processObjectSchema(
|
|
775
780
|
schema: Record<string, any> | string,
|
|
776
781
|
parentElement: HTMLElement | null = null,
|
|
777
|
-
options: LayoutOptions = {}
|
|
782
|
+
options: LayoutOptions = {},
|
|
778
783
|
): LayoutResult {
|
|
779
784
|
const layout: Record<string, any> = {};
|
|
780
785
|
const defaultCreator = options.creator || createElement;
|
|
@@ -792,7 +797,7 @@ function processObjectSchema(
|
|
|
792
797
|
|
|
793
798
|
const rootComponent = createComponentInstance(
|
|
794
799
|
createElementFn,
|
|
795
|
-
processedOptions
|
|
800
|
+
processedOptions,
|
|
796
801
|
);
|
|
797
802
|
layout.element = rootComponent;
|
|
798
803
|
if (elementDef.name) layout[elementDef.name] = rootComponent;
|
|
@@ -805,7 +810,7 @@ function processObjectSchema(
|
|
|
805
810
|
const childResult = processObjectSchema(
|
|
806
811
|
elementDef.children,
|
|
807
812
|
rootElement,
|
|
808
|
-
options
|
|
813
|
+
options,
|
|
809
814
|
);
|
|
810
815
|
Object.assign(layout, childResult.layout);
|
|
811
816
|
}
|
|
@@ -908,14 +913,47 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
|
|
|
908
913
|
},
|
|
909
914
|
|
|
910
915
|
destroy(): void {
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
+
// Track destroyed components to avoid double-destroy
|
|
917
|
+
const destroyed = new Set<any>();
|
|
918
|
+
|
|
919
|
+
// Helper to safely destroy a component
|
|
920
|
+
const destroyComponent = (component: any): void => {
|
|
921
|
+
if (!component || destroyed.has(component)) return;
|
|
922
|
+
|
|
923
|
+
// Skip plain HTML elements and non-objects
|
|
924
|
+
if (component instanceof HTMLElement || typeof component !== "object")
|
|
925
|
+
return;
|
|
926
|
+
|
|
927
|
+
// Check if it's a component with destroy method
|
|
928
|
+
if (typeof component.destroy === "function") {
|
|
929
|
+
destroyed.add(component);
|
|
930
|
+
try {
|
|
931
|
+
component.destroy();
|
|
932
|
+
} catch (e) {
|
|
933
|
+
// Ignore destroy errors - component may already be cleaned up
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// First destroy components from the components array (if present)
|
|
939
|
+
// This ensures all named components are destroyed even if nested
|
|
940
|
+
if (Array.isArray(layout.components)) {
|
|
941
|
+
for (const [name, component] of layout.components) {
|
|
942
|
+
destroyComponent(component);
|
|
916
943
|
}
|
|
917
944
|
}
|
|
918
945
|
|
|
946
|
+
// Then iterate over all layout keys for any missed components
|
|
947
|
+
for (const key in layout) {
|
|
948
|
+
if (key === "element" || key === "components") continue;
|
|
949
|
+
destroyComponent(layout[key]);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Clear the components array
|
|
953
|
+
if (Array.isArray(layout.components)) {
|
|
954
|
+
layout.components.length = 0;
|
|
955
|
+
}
|
|
956
|
+
|
|
919
957
|
// Remove root element from DOM
|
|
920
958
|
if (layout.element) {
|
|
921
959
|
const element = isComponent(layout.element)
|
|
@@ -925,6 +963,11 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
|
|
|
925
963
|
element.parentNode.removeChild(element);
|
|
926
964
|
}
|
|
927
965
|
}
|
|
966
|
+
|
|
967
|
+
// Clear references to help GC
|
|
968
|
+
for (const key in layout) {
|
|
969
|
+
delete layout[key];
|
|
970
|
+
}
|
|
928
971
|
},
|
|
929
972
|
};
|
|
930
973
|
}
|
|
@@ -940,7 +983,7 @@ function createLayoutResult(layout: Record<string, any>): LayoutResult {
|
|
|
940
983
|
export function createLayout(
|
|
941
984
|
schema: any,
|
|
942
985
|
parentElement: HTMLElement | null = null,
|
|
943
|
-
options: LayoutOptions = {}
|
|
986
|
+
options: LayoutOptions = {},
|
|
944
987
|
): LayoutResult {
|
|
945
988
|
// Handle function schemas
|
|
946
989
|
if (typeof schema === "function") {
|
|
@@ -32,7 +32,7 @@ export const VIEWPORT_CONSTANTS = {
|
|
|
32
32
|
|
|
33
33
|
// Loading settings
|
|
34
34
|
LOADING: {
|
|
35
|
-
CANCEL_THRESHOLD:
|
|
35
|
+
CANCEL_THRESHOLD: 25, // px/ms - velocity above which loads cancel
|
|
36
36
|
MAX_CONCURRENT_REQUESTS: 1, // Parallel requests allowed
|
|
37
37
|
DEFAULT_RANGE_SIZE: 20, // Items per request
|
|
38
38
|
DEBOUNCE_LOADING: 150, // Debounce delay (ms)
|
|
@@ -86,6 +86,11 @@ export const VIEWPORT_CONSTANTS = {
|
|
|
86
86
|
LOADING_DELAY: 100, // ms - delay before showing loading state
|
|
87
87
|
},
|
|
88
88
|
|
|
89
|
+
// Selection settings
|
|
90
|
+
SELECTION: {
|
|
91
|
+
SELECTED_CLASS: "viewport-item--selected",
|
|
92
|
+
},
|
|
93
|
+
|
|
89
94
|
// Scrollbar settings (from list-manager)
|
|
90
95
|
SCROLLBAR: {
|
|
91
96
|
// CSS classes
|
|
@@ -131,7 +136,7 @@ export type ViewportConstants = typeof VIEWPORT_CONSTANTS;
|
|
|
131
136
|
* Helper function to merge user constants with defaults
|
|
132
137
|
*/
|
|
133
138
|
export function mergeConstants(
|
|
134
|
-
userConstants: Partial<ViewportConstants> = {}
|
|
139
|
+
userConstants: Partial<ViewportConstants> = {},
|
|
135
140
|
): ViewportConstants {
|
|
136
141
|
return {
|
|
137
142
|
...VIEWPORT_CONSTANTS,
|