@lightningtv/solid 3.0.0-17 → 3.0.0-19
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/README.md +6 -0
- package/dist/src/primitives/Column.jsx +8 -7
- package/dist/src/primitives/Column.jsx.map +1 -1
- package/dist/src/primitives/Image.d.ts +8 -0
- package/dist/src/primitives/Image.jsx +24 -0
- package/dist/src/primitives/Image.jsx.map +1 -0
- package/dist/src/primitives/KeepAlive.d.ts +19 -5
- package/dist/src/primitives/KeepAlive.jsx +52 -21
- package/dist/src/primitives/KeepAlive.jsx.map +1 -1
- package/dist/src/primitives/Lazy.d.ts +6 -7
- package/dist/src/primitives/Lazy.jsx +23 -20
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/Row.jsx +8 -7
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Virtual.d.ts +18 -0
- package/dist/src/primitives/Virtual.jsx +428 -0
- package/dist/src/primitives/Virtual.jsx.map +1 -0
- package/dist/src/primitives/VirtualGrid.d.ts +13 -0
- package/dist/src/primitives/VirtualGrid.jsx +139 -0
- package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
- package/dist/src/primitives/VirtualList.d.ts +11 -0
- package/dist/src/primitives/VirtualList.jsx +96 -0
- package/dist/src/primitives/VirtualList.jsx.map +1 -0
- package/dist/src/primitives/VirtualRow.d.ts +13 -0
- package/dist/src/primitives/VirtualRow.jsx +97 -0
- package/dist/src/primitives/VirtualRow.jsx.map +1 -0
- package/dist/src/primitives/Visible.d.ts +0 -1
- package/dist/src/primitives/Visible.jsx +1 -1
- package/dist/src/primitives/Visible.jsx.map +1 -1
- package/dist/src/primitives/createFocusStack.d.ts +4 -4
- package/dist/src/primitives/createFocusStack.jsx +15 -6
- package/dist/src/primitives/createFocusStack.jsx.map +1 -1
- package/dist/src/primitives/index.d.ts +5 -1
- package/dist/src/primitives/index.js +5 -1
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +1 -0
- package/dist/src/primitives/useMouse.d.ts +6 -0
- package/dist/src/primitives/useMouse.js +26 -3
- package/dist/src/primitives/useMouse.js.map +1 -1
- package/dist/src/primitives/utils/handleNavigation.d.ts +0 -1
- package/dist/src/primitives/utils/handleNavigation.js +7 -5
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +5 -1
- package/dist/src/primitives/utils/withScrolling.js +11 -5
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +1 -0
- package/dist/src/render.js +4 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/universal.d.ts +25 -0
- package/dist/src/universal.js +232 -0
- package/dist/src/universal.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +8 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/src/primitives/Column.tsx +10 -9
- package/src/primitives/Image.tsx +36 -0
- package/src/primitives/KeepAlive.tsx +124 -0
- package/src/primitives/Lazy.tsx +34 -38
- package/src/primitives/Row.tsx +11 -9
- package/src/primitives/Virtual.tsx +471 -0
- package/src/primitives/VirtualGrid.tsx +199 -0
- package/src/primitives/Visible.tsx +1 -2
- package/src/primitives/createFocusStack.tsx +18 -7
- package/src/primitives/index.ts +5 -1
- package/src/primitives/types.ts +1 -0
- package/src/primitives/useMouse.ts +52 -7
- package/src/primitives/utils/handleNavigation.ts +8 -5
- package/src/primitives/utils/withScrolling.ts +22 -14
- package/src/render.ts +5 -0
- package/src/utils.ts +10 -0
package/src/primitives/Lazy.tsx
CHANGED
|
@@ -1,56 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
createRenderEffect,
|
|
5
|
-
createMemo,
|
|
6
|
-
createSignal,
|
|
7
|
-
createReaction,
|
|
8
|
-
Show,
|
|
9
|
-
type JSX,
|
|
10
|
-
type ValidComponent,
|
|
11
|
-
untrack,
|
|
12
|
-
type Accessor,
|
|
13
|
-
} from 'solid-js'; // Dynamic removed
|
|
14
|
-
import { type NewOmit, scheduleTask, type NodeProps, Dynamic, ElementNode } from '@lightningtv/solid'; // Dynamic removed from imports
|
|
15
|
-
import { Row, Column } from '@lightningtv/solid/primitives';
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
16
4
|
|
|
17
|
-
type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
|
|
5
|
+
type LazyProps<T extends readonly any[]> = lng.NewOmit<lng.NodeProps, 'children'> & {
|
|
18
6
|
each: T | undefined | null | false;
|
|
19
|
-
fallback?: JSX.Element;
|
|
20
7
|
upCount: number;
|
|
21
8
|
buffer?: number;
|
|
22
9
|
delay?: number;
|
|
23
10
|
sync?: boolean;
|
|
24
11
|
eagerLoad?: boolean;
|
|
25
|
-
children: (item: Accessor<T[number]>, index: number) => JSX.Element;
|
|
12
|
+
children: (item: s.Accessor<T[number]>, index: number) => s.JSX.Element;
|
|
26
13
|
};
|
|
27
14
|
|
|
28
15
|
function createLazy<T>(
|
|
29
|
-
component: ValidComponent,
|
|
16
|
+
component: s.ValidComponent,
|
|
30
17
|
props: LazyProps<readonly T[]>,
|
|
31
|
-
keyHandler: (updateOffset: (event: KeyboardEvent, container: ElementNode) => void) => Record<string, (event: KeyboardEvent, container: ElementNode) => void>
|
|
18
|
+
keyHandler: (updateOffset: (event: KeyboardEvent, container: lng.ElementNode) => void) => Record<string, (event: KeyboardEvent, container: lng.ElementNode) => void>
|
|
32
19
|
) {
|
|
33
20
|
// Need at least one item so it can be focused
|
|
34
|
-
const [offset, setOffset] = createSignal<number>(props.sync ? props.upCount : 0);
|
|
21
|
+
const [offset, setOffset] = s.createSignal<number>(props.sync ? props.upCount : 0);
|
|
35
22
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
23
|
+
let viewRef!: lngp.NavigableElement;
|
|
36
24
|
|
|
37
|
-
const buffer = createMemo(() => {
|
|
25
|
+
const buffer = s.createMemo(() => {
|
|
38
26
|
if (typeof props.buffer === 'number') {
|
|
39
27
|
return props.buffer;
|
|
40
28
|
}
|
|
41
29
|
const scroll = props.scroll || props.style?.scroll;
|
|
42
|
-
if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount;
|
|
43
|
-
if (scroll === 'center') return Math.ceil(props.upCount / 2);
|
|
30
|
+
if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount + 1;
|
|
31
|
+
if (scroll === 'center') return Math.ceil(props.upCount / 2) + 1;
|
|
44
32
|
return 2;
|
|
45
33
|
});
|
|
46
34
|
|
|
47
|
-
createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
|
|
35
|
+
s.createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
|
|
48
36
|
|
|
49
|
-
if (!props.sync || props.
|
|
50
|
-
createEffect(() => {
|
|
37
|
+
if (!props.sync || props.eagerLoad) {
|
|
38
|
+
s.createEffect(() => {
|
|
51
39
|
if (props.each) {
|
|
52
40
|
const loadItems = () => {
|
|
53
|
-
let count = untrack(offset);
|
|
41
|
+
let count = s.untrack(offset);
|
|
54
42
|
if (count < props.upCount) {
|
|
55
43
|
setOffset(count + 1);
|
|
56
44
|
timeoutId = setTimeout(loadItems, 16); // ~60fps
|
|
@@ -59,7 +47,7 @@ function createLazy<T>(
|
|
|
59
47
|
const maxOffset = props.each ? props.each.length : 0;
|
|
60
48
|
if (count >= maxOffset) return;
|
|
61
49
|
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
62
|
-
scheduleTask(loadItems);
|
|
50
|
+
lng.scheduleTask(loadItems);
|
|
63
51
|
}
|
|
64
52
|
};
|
|
65
53
|
loadItems();
|
|
@@ -67,11 +55,16 @@ function createLazy<T>(
|
|
|
67
55
|
});
|
|
68
56
|
}
|
|
69
57
|
|
|
70
|
-
const items = createMemo(() => (
|
|
58
|
+
const items: s.Accessor<T[]> = s.createMemo(() => (
|
|
71
59
|
Array.isArray(props.each) ? props.each.slice(0, offset()) : [])
|
|
72
60
|
);
|
|
73
61
|
|
|
74
|
-
|
|
62
|
+
function lazyScrollToIndex(this: lngp.NavigableElement, index: number) {
|
|
63
|
+
setOffset(Math.max(index, 0) + buffer())
|
|
64
|
+
queueMicrotask(() => viewRef.scrollToIndex(index));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
|
|
75
68
|
const maxOffset = props.each ? props.each.length : 0;
|
|
76
69
|
const selected = container.selected || 0;
|
|
77
70
|
const numChildren = container.children.length;
|
|
@@ -97,18 +90,21 @@ function createLazy<T>(
|
|
|
97
90
|
const handler = keyHandler(updateOffset);
|
|
98
91
|
|
|
99
92
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
<lng.Dynamic
|
|
94
|
+
{...props}
|
|
95
|
+
component={component}
|
|
96
|
+
{/* @once */ ...handler}
|
|
97
|
+
lazyScrollToIndex={lazyScrollToIndex}
|
|
98
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)} >
|
|
99
|
+
<s.Index each={items()} children={props.children} />
|
|
100
|
+
</lng.Dynamic>
|
|
105
101
|
);
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
export function LazyRow<T extends readonly any[]>(props: LazyProps<T>) {
|
|
109
|
-
return createLazy(Row, props, (updateOffset) => ({ onRight: updateOffset }));
|
|
105
|
+
return createLazy(lngp.Row, props, (updateOffset) => ({ onRight: updateOffset }));
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
export function LazyColumn<T extends readonly any[]>(props: LazyProps<T>) {
|
|
113
|
-
return createLazy(Column, props, (updateOffset) => ({ onDown: updateOffset }));
|
|
109
|
+
return createLazy(lngp.Column, props, (updateOffset) => ({ onDown: updateOffset }));
|
|
114
110
|
}
|
package/src/primitives/Row.tsx
CHANGED
|
@@ -2,10 +2,11 @@ import { type Component } from 'solid-js';
|
|
|
2
2
|
import { combineStyles, type NodeStyles, type ElementNode } from '@lightningtv/solid';
|
|
3
3
|
import { chainFunctions } from './utils/chainFunctions.js';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
handleNavigation,
|
|
6
|
+
navigableForwardFocus
|
|
6
7
|
} from './utils/handleNavigation.js';
|
|
7
|
-
import { withScrolling } from './utils/withScrolling.js';
|
|
8
8
|
import type { RowProps } from './types.js';
|
|
9
|
+
import { scrollRow } from './utils/withScrolling.js';
|
|
9
10
|
|
|
10
11
|
const RowStyles: NodeStyles = {
|
|
11
12
|
display: 'flex',
|
|
@@ -18,31 +19,32 @@ const RowStyles: NodeStyles = {
|
|
|
18
19
|
},
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
const scroll = withScrolling(true);
|
|
22
|
-
|
|
23
22
|
function scrollToIndex(this: ElementNode, index: number) {
|
|
24
23
|
this.selected = index;
|
|
25
|
-
|
|
24
|
+
scrollRow(index, this);
|
|
26
25
|
this.children[index]?.setFocus();
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
const onLeft = handleNavigation('left');
|
|
29
|
+
const onRight = handleNavigation('right');
|
|
30
|
+
|
|
29
31
|
export const Row: Component<RowProps> = (props) => {
|
|
30
32
|
return (
|
|
31
33
|
<view
|
|
32
34
|
{...props}
|
|
33
35
|
selected={props.selected || 0}
|
|
34
|
-
onLeft={/* @once */ chainFunctions(props.onLeft,
|
|
35
|
-
onRight={/* @once */ chainFunctions(props.onRight,
|
|
36
|
+
onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
|
|
37
|
+
onRight={/* @once */ chainFunctions(props.onRight, onRight)}
|
|
36
38
|
forwardFocus={navigableForwardFocus}
|
|
37
39
|
scrollToIndex={scrollToIndex}
|
|
38
40
|
onLayout={
|
|
39
41
|
/* @once */
|
|
40
|
-
props.selected ? chainFunctions(props.onLayout,
|
|
42
|
+
props.selected ? chainFunctions(props.onLayout, scrollRow) : props.onLayout
|
|
41
43
|
}
|
|
42
44
|
onSelectedChanged={
|
|
43
45
|
/* @once */ chainFunctions(
|
|
44
46
|
props.onSelectedChanged,
|
|
45
|
-
props.scroll !== 'none' ?
|
|
47
|
+
props.scroll !== 'none' ? scrollRow : undefined,
|
|
46
48
|
)
|
|
47
49
|
}
|
|
48
50
|
style={/* @once */ combineStyles(props.style, RowStyles)}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
4
|
+
import { List } from '@solid-primitives/list';
|
|
5
|
+
import * as utils from '../utils.js';
|
|
6
|
+
|
|
7
|
+
export type VirtualProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
|
|
8
|
+
each: readonly T[] | undefined | null | false;
|
|
9
|
+
displaySize: number;
|
|
10
|
+
bufferSize?: number;
|
|
11
|
+
wrap?: boolean;
|
|
12
|
+
scrollIndex?: number;
|
|
13
|
+
onEndReached?: () => void;
|
|
14
|
+
onEndReachedThreshold?: number;
|
|
15
|
+
debugInfo?: boolean;
|
|
16
|
+
factorScale?: boolean;
|
|
17
|
+
uniformSize?: boolean;
|
|
18
|
+
children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function createVirtual<T>(
|
|
22
|
+
component: typeof lngp.Row | typeof lngp.Column,
|
|
23
|
+
props: VirtualProps<T>,
|
|
24
|
+
keyHandlers: Record<string, lng.KeyHandler>
|
|
25
|
+
) {
|
|
26
|
+
const isRow = component === lngp.Row;
|
|
27
|
+
const axis = isRow ? 'x' : 'y';
|
|
28
|
+
const [cursor, setCursor] = s.createSignal(props.selected ?? 0);
|
|
29
|
+
const bufferSize = s.createMemo(() => props.bufferSize || 2);
|
|
30
|
+
const scrollIndex = s.createMemo(() => props.scrollIndex || 0);
|
|
31
|
+
const items = s.createMemo(() => props.each || []);
|
|
32
|
+
const itemCount = s.createMemo(() => items().length);
|
|
33
|
+
const scrollType = s.createMemo(() => props.scroll || 'auto');
|
|
34
|
+
|
|
35
|
+
const selected = () => {
|
|
36
|
+
if (props.wrap) {
|
|
37
|
+
return Math.max(bufferSize(), scrollIndex());
|
|
38
|
+
}
|
|
39
|
+
return props.selected || 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let cachedScaledSize: number | undefined;
|
|
43
|
+
let targetPosition: number | undefined;
|
|
44
|
+
let cachedAnimationController: lng.IAnimationController | undefined;
|
|
45
|
+
const uniformSize = s.createMemo(() => {
|
|
46
|
+
return props.uniformSize !== false;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
type SliceState = { start: number; slice: T[]; selected: number, delta: number, shiftBy: number, atStart: boolean };
|
|
50
|
+
const [slice, setSlice] = s.createSignal<SliceState>({
|
|
51
|
+
start: 0,
|
|
52
|
+
slice: [],
|
|
53
|
+
selected: 0,
|
|
54
|
+
delta: 0,
|
|
55
|
+
shiftBy: 0,
|
|
56
|
+
atStart: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function normalizeDeltaForWindow(delta: number, windowLen: number): number {
|
|
60
|
+
if (!windowLen) return 0;
|
|
61
|
+
const half = windowLen / 2;
|
|
62
|
+
if (delta > half) return delta - windowLen;
|
|
63
|
+
if (delta < -half) return delta + windowLen;
|
|
64
|
+
return delta;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeSize(selected: number = 0) {
|
|
68
|
+
if (uniformSize() && cachedScaledSize) {
|
|
69
|
+
return cachedScaledSize;
|
|
70
|
+
} else if (viewRef) {
|
|
71
|
+
const gap = viewRef.gap || 0;
|
|
72
|
+
const dimension = isRow ? 'width' : 'height'; // This can't be moved up as it depends on viewRef
|
|
73
|
+
const prevSelectedChild = viewRef.children[selected];
|
|
74
|
+
|
|
75
|
+
if (prevSelectedChild instanceof lng.ElementNode) {
|
|
76
|
+
const itemSize = prevSelectedChild[dimension] || 0;
|
|
77
|
+
const focusStyle = (prevSelectedChild.style?.focus as lng.NodeStyles);
|
|
78
|
+
const scale = (focusStyle?.scale ?? prevSelectedChild.scale ?? 1);
|
|
79
|
+
const scaledSize = itemSize * (props.factorScale ? scale : 1) + gap;
|
|
80
|
+
cachedScaledSize = scaledSize;
|
|
81
|
+
return scaledSize;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function computeSlice(c: number, delta: number, prev: SliceState): SliceState {
|
|
88
|
+
const total = itemCount();
|
|
89
|
+
if (total === 0) return { start: 0, slice: [], selected: 0, delta, shiftBy: 0, atStart: true };
|
|
90
|
+
|
|
91
|
+
const length = props.displaySize + bufferSize();
|
|
92
|
+
let start = prev.start;
|
|
93
|
+
let selected = prev.selected;
|
|
94
|
+
let atStart = prev.atStart;
|
|
95
|
+
let shiftBy = -delta;
|
|
96
|
+
|
|
97
|
+
switch (scrollType()) {
|
|
98
|
+
case 'always':
|
|
99
|
+
if (props.wrap) {
|
|
100
|
+
start = utils.mod(c - 1, total);
|
|
101
|
+
selected = 1;
|
|
102
|
+
} else {
|
|
103
|
+
start = utils.clamp(
|
|
104
|
+
c - bufferSize(),
|
|
105
|
+
0,
|
|
106
|
+
Math.max(0, total - props.displaySize - bufferSize()),
|
|
107
|
+
);
|
|
108
|
+
if (delta === 0 && c > 3) {
|
|
109
|
+
shiftBy = c < 3 ? -c : -2;
|
|
110
|
+
selected = 2;
|
|
111
|
+
} else {
|
|
112
|
+
selected =
|
|
113
|
+
c < bufferSize()
|
|
114
|
+
? c
|
|
115
|
+
: c >= total - props.displaySize
|
|
116
|
+
? c - (total - props.displaySize) + bufferSize()
|
|
117
|
+
: bufferSize();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'auto':
|
|
123
|
+
if (props.wrap) {
|
|
124
|
+
if (delta === 0) {
|
|
125
|
+
selected = scrollIndex() || 1;
|
|
126
|
+
start = utils.mod(c - (scrollIndex() || 1), total);
|
|
127
|
+
} else {
|
|
128
|
+
start = utils.mod(c - (prev.selected || 1), total);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
if (delta < 0) {
|
|
132
|
+
// Moving left
|
|
133
|
+
if (prev.start > 0 && prev.selected >= props.displaySize) {
|
|
134
|
+
// Move selection left inside slice
|
|
135
|
+
start = prev.start;
|
|
136
|
+
selected = prev.selected - 1;
|
|
137
|
+
} else if (prev.start > 0) {
|
|
138
|
+
// Move selection left inside slice
|
|
139
|
+
start = prev.start - 1;
|
|
140
|
+
selected = prev.selected;
|
|
141
|
+
// shiftBy = 0;
|
|
142
|
+
} else if (prev.start === 0 && !prev.atStart) {
|
|
143
|
+
start = 0;
|
|
144
|
+
selected = prev.selected - 1;
|
|
145
|
+
atStart = true;
|
|
146
|
+
} else if (selected >= props.displaySize - 1) {
|
|
147
|
+
// Shift window left, keep selection pinned
|
|
148
|
+
start = 0;
|
|
149
|
+
selected = prev.selected - 1;
|
|
150
|
+
} else {
|
|
151
|
+
start = 0;
|
|
152
|
+
selected = prev.selected - 1;
|
|
153
|
+
shiftBy = 0;
|
|
154
|
+
}
|
|
155
|
+
} else if (delta > 0) {
|
|
156
|
+
// Moving right
|
|
157
|
+
if (prev.selected < scrollIndex()) {
|
|
158
|
+
// Move selection right inside slice
|
|
159
|
+
start = prev.start;
|
|
160
|
+
selected = prev.selected + 1;
|
|
161
|
+
shiftBy = 0;
|
|
162
|
+
} else if (prev.selected === scrollIndex() || atStart) {
|
|
163
|
+
start = prev.start;
|
|
164
|
+
selected = prev.selected + 1;
|
|
165
|
+
atStart = false;
|
|
166
|
+
} else if (prev.start === 0 && prev.selected === 0) {
|
|
167
|
+
start = 0;
|
|
168
|
+
selected = 1;
|
|
169
|
+
atStart = false;
|
|
170
|
+
} else if (prev.start >= total - props.displaySize) {
|
|
171
|
+
// At end: clamp slice, selection drifts right
|
|
172
|
+
start = prev.start;
|
|
173
|
+
selected = c - start;
|
|
174
|
+
shiftBy = 0;
|
|
175
|
+
} else {
|
|
176
|
+
// Shift window right, keep selection pinned
|
|
177
|
+
start = prev.start + 1;
|
|
178
|
+
selected = Math.max(prev.selected, scrollIndex() + 1);;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Initial setup
|
|
182
|
+
if (c > 0) {
|
|
183
|
+
start = Math.min(c - (scrollIndex() || 1), total - props.displaySize - bufferSize());
|
|
184
|
+
selected = Math.max(scrollIndex() || 1, c - start);
|
|
185
|
+
shiftBy = total - c < 3 ? c - total : -1;
|
|
186
|
+
atStart = false;
|
|
187
|
+
} else {
|
|
188
|
+
start = prev.start;
|
|
189
|
+
selected = prev.selected;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'edge':
|
|
196
|
+
const startScrolling = Math.max(1, props.displaySize + (atStart ? -1 : 0));
|
|
197
|
+
if (props.wrap) {
|
|
198
|
+
if (delta > 0) {
|
|
199
|
+
if (prev.selected < startScrolling) {
|
|
200
|
+
selected = prev.selected + 1;
|
|
201
|
+
shiftBy = 0;
|
|
202
|
+
} else if (prev.selected === startScrolling && atStart) {
|
|
203
|
+
selected = prev.selected + 1;
|
|
204
|
+
atStart = false;
|
|
205
|
+
} else {
|
|
206
|
+
start = utils.mod(prev.start + 1, total);
|
|
207
|
+
selected = prev.selected;
|
|
208
|
+
}
|
|
209
|
+
} else if (delta < 0) {
|
|
210
|
+
if (prev.selected > 1) {
|
|
211
|
+
selected = prev.selected - 1;
|
|
212
|
+
shiftBy = 0;
|
|
213
|
+
} else {
|
|
214
|
+
start = utils.mod(prev.start - 1, total);
|
|
215
|
+
selected = 1;
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
start = utils.mod(c - 1, total);
|
|
219
|
+
selected = 1;
|
|
220
|
+
shiftBy = -1;
|
|
221
|
+
atStart = false;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
if (delta === 0 && c > 0) {
|
|
225
|
+
//initial setup
|
|
226
|
+
selected = c > startScrolling ? startScrolling : c;
|
|
227
|
+
start = Math.max(0, c - startScrolling + 1);
|
|
228
|
+
shiftBy = c > startScrolling ? -1 : 0;
|
|
229
|
+
atStart = c < startScrolling;
|
|
230
|
+
} else if (delta > 0) {
|
|
231
|
+
if (prev.selected < startScrolling) {
|
|
232
|
+
selected = prev.selected + 1;
|
|
233
|
+
shiftBy = 0;
|
|
234
|
+
} else if (prev.selected === startScrolling && atStart) {
|
|
235
|
+
selected = prev.selected + 1;
|
|
236
|
+
atStart = false;
|
|
237
|
+
} else {
|
|
238
|
+
start = prev.start + 1;
|
|
239
|
+
selected = prev.selected;
|
|
240
|
+
atStart = false;
|
|
241
|
+
}
|
|
242
|
+
} else if (delta < 0) {
|
|
243
|
+
if (prev.selected > 1) {
|
|
244
|
+
selected = prev.selected - 1;
|
|
245
|
+
shiftBy = 0;
|
|
246
|
+
} else if (c > 1) {
|
|
247
|
+
start = Math.max(0, c - 1);
|
|
248
|
+
selected = 1;
|
|
249
|
+
} else if (c === 1) {
|
|
250
|
+
start = 0;
|
|
251
|
+
selected = 1;
|
|
252
|
+
} else {
|
|
253
|
+
start = 0;
|
|
254
|
+
selected = 0;
|
|
255
|
+
shiftBy = atStart ? 0 : shiftBy;
|
|
256
|
+
atStart = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'none':
|
|
263
|
+
default:
|
|
264
|
+
start = 0;
|
|
265
|
+
selected = c;
|
|
266
|
+
shiftBy = 0;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let newSlice = prev.slice;
|
|
271
|
+
if (start !== prev.start || newSlice.length === 0) {
|
|
272
|
+
newSlice = props.wrap
|
|
273
|
+
? Array.from(
|
|
274
|
+
{ length },
|
|
275
|
+
(_, i) => items()[utils.mod(start + i, total)],
|
|
276
|
+
) as T[]
|
|
277
|
+
: items().slice(start, start + length);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const state: SliceState = { start, slice: newSlice, selected, delta, shiftBy, atStart };
|
|
281
|
+
|
|
282
|
+
if (props.debugInfo) {
|
|
283
|
+
console.log(`[Virtual]`, {
|
|
284
|
+
cursor: c,
|
|
285
|
+
delta,
|
|
286
|
+
start,
|
|
287
|
+
selected,
|
|
288
|
+
shiftBy,
|
|
289
|
+
slice: state.slice,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let viewRef!: lngp.NavigableElement;
|
|
297
|
+
|
|
298
|
+
function scrollToIndex(this: lng.ElementNode, index: number) {
|
|
299
|
+
s.untrack(() => {
|
|
300
|
+
if (itemCount() === 0) return;
|
|
301
|
+
|
|
302
|
+
lastNavTime = performance.now();
|
|
303
|
+
if (originalPosition !== undefined) {
|
|
304
|
+
viewRef.lng[axis] = originalPosition;
|
|
305
|
+
targetPosition = originalPosition;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
updateSelected([utils.clamp(index, 0, itemCount() - 1)]);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let lastNavTime = 0;
|
|
313
|
+
function getAdaptiveDuration(duration: number = 250) {
|
|
314
|
+
const now = performance.now();
|
|
315
|
+
const delta = now - lastNavTime;
|
|
316
|
+
lastNavTime = now;
|
|
317
|
+
if (delta < duration) return delta;
|
|
318
|
+
return duration;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let originalPosition: number | undefined;
|
|
322
|
+
const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, _active, _lastIdx) {
|
|
323
|
+
let idx = _idx;
|
|
324
|
+
let lastIdx = _lastIdx || 0;
|
|
325
|
+
let active = _active;
|
|
326
|
+
const noChange = idx === lastIdx;
|
|
327
|
+
const total = itemCount();
|
|
328
|
+
originalPosition = originalPosition ?? elm[axis];
|
|
329
|
+
|
|
330
|
+
if (props.onSelectedChanged) {
|
|
331
|
+
props.onSelectedChanged.call(this as lngp.NavigableElement, idx, this as lngp.NavigableElement, active, lastIdx);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (noChange) return;
|
|
335
|
+
|
|
336
|
+
const rawDelta = idx - (lastIdx ?? 0);
|
|
337
|
+
const windowLen =
|
|
338
|
+
elm?.children?.length ?? props.displaySize + bufferSize();
|
|
339
|
+
const delta = props.wrap
|
|
340
|
+
? normalizeDeltaForWindow(rawDelta, windowLen)
|
|
341
|
+
: rawDelta;
|
|
342
|
+
|
|
343
|
+
setCursor(c => {
|
|
344
|
+
const next = c + delta;
|
|
345
|
+
return props.wrap
|
|
346
|
+
? utils.mod(next, total)
|
|
347
|
+
: utils.clamp(next, 0, total - 1);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const newState = computeSlice(cursor(), delta, slice());
|
|
351
|
+
setSlice(newState);
|
|
352
|
+
elm.selected = newState.selected;
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
props.onEndReachedThreshold !== undefined &&
|
|
356
|
+
cursor() >= itemCount() - props.onEndReachedThreshold
|
|
357
|
+
) {
|
|
358
|
+
props.onEndReached?.();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (newState.shiftBy === 0) return;
|
|
362
|
+
|
|
363
|
+
const prevChildPos = (targetPosition ?? this[axis]) + active[axis];
|
|
364
|
+
|
|
365
|
+
queueMicrotask(() => {
|
|
366
|
+
elm.updateLayout();
|
|
367
|
+
const childSize = computeSize(slice().selected);
|
|
368
|
+
|
|
369
|
+
if (cachedAnimationController && cachedAnimationController.state === 'running') {
|
|
370
|
+
cachedAnimationController.stop();;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (lng.Config.animationsEnabled) {
|
|
374
|
+
this.lng[axis] = prevChildPos - active[axis];
|
|
375
|
+
let offset = this.lng[axis] + (childSize * slice().shiftBy);
|
|
376
|
+
targetPosition = offset;
|
|
377
|
+
cachedAnimationController = this.animate(
|
|
378
|
+
{ [axis]: offset },
|
|
379
|
+
{ ...this.animationSettings, duration: getAdaptiveDuration(this.animationSettings?.duration)}
|
|
380
|
+
).start();
|
|
381
|
+
} else {
|
|
382
|
+
this.lng[axis] = this.lng[axis]! + (childSize * slice().shiftBy);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const updateSelected = ([sel, _items]: [number?, any?]) => {
|
|
388
|
+
if (!viewRef || sel === undefined || itemCount() === 0) return;
|
|
389
|
+
const item = items()[sel];
|
|
390
|
+
setCursor(sel);
|
|
391
|
+
const newState = computeSlice(cursor(), 0, slice());
|
|
392
|
+
setSlice(newState);
|
|
393
|
+
|
|
394
|
+
queueMicrotask(() => {
|
|
395
|
+
viewRef.updateLayout();
|
|
396
|
+
let activeIndex = viewRef.children.findIndex(x => x.item === item);
|
|
397
|
+
if (activeIndex === -1) return;
|
|
398
|
+
viewRef.selected = activeIndex;
|
|
399
|
+
viewRef.children[activeIndex]?.setFocus();
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
let doOnce = false;
|
|
404
|
+
s.createEffect(s.on([() => props.wrap, items], () => {
|
|
405
|
+
if (!viewRef || itemCount() === 0 || !props.wrap || doOnce) return;
|
|
406
|
+
doOnce = true;
|
|
407
|
+
// offset just for wrap so we keep one item before
|
|
408
|
+
queueMicrotask(() => {
|
|
409
|
+
const childSize = computeSize(slice().selected);
|
|
410
|
+
viewRef.lng[axis] = (viewRef.lng[axis] || 0) + (childSize * -1);
|
|
411
|
+
// Original Position is offset to support scrollToIndex
|
|
412
|
+
originalPosition = viewRef.lng[axis];
|
|
413
|
+
targetPosition = viewRef.lng[axis];
|
|
414
|
+
});
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
s.createEffect(s.on([() => props.selected, items], updateSelected));
|
|
418
|
+
|
|
419
|
+
s.createEffect(s.on(items, () => {
|
|
420
|
+
if (!viewRef || itemCount() === 0) return;
|
|
421
|
+
if (cursor() >= itemCount()) {
|
|
422
|
+
setCursor(itemCount() - 1);
|
|
423
|
+
}
|
|
424
|
+
const newState = computeSlice(cursor(), 0, slice());
|
|
425
|
+
setSlice(newState);
|
|
426
|
+
viewRef.selected = newState.selected;
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
return (<view
|
|
430
|
+
{...props}
|
|
431
|
+
{...keyHandlers}
|
|
432
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
|
|
433
|
+
selected={selected()}
|
|
434
|
+
cursor={cursor()}
|
|
435
|
+
forwardFocus={/* @once */ lngp.navigableForwardFocus}
|
|
436
|
+
scrollToIndex={/* @once */ scrollToIndex}
|
|
437
|
+
onSelectedChanged={/* @once */ onSelectedChanged}
|
|
438
|
+
style={/* @once */ lng.combineStyles(
|
|
439
|
+
props.style,
|
|
440
|
+
component === lngp.Row
|
|
441
|
+
? {
|
|
442
|
+
display: 'flex',
|
|
443
|
+
gap: 30,
|
|
444
|
+
transition: { x: { duration: 250, easing: 'ease-out' } },
|
|
445
|
+
}
|
|
446
|
+
: {
|
|
447
|
+
display: 'flex',
|
|
448
|
+
flexDirection: 'column',
|
|
449
|
+
gap: 30,
|
|
450
|
+
transition: { y: { duration: 250, easing: 'ease-out' } },
|
|
451
|
+
}
|
|
452
|
+
)}
|
|
453
|
+
>
|
|
454
|
+
<List each={slice().slice}>{props.children}</List>
|
|
455
|
+
</view>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function VirtualRow<T>(props: VirtualProps<T>) {
|
|
460
|
+
return createVirtual(lngp.Row, props, {
|
|
461
|
+
onLeft: lngp.chainFunctions(props.onLeft, lngp.handleNavigation('left')) as lng.KeyHandler,
|
|
462
|
+
onRight: lngp.chainFunctions(props.onRight, lngp.handleNavigation('right')) as lng.KeyHandler,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function VirtualColumn<T>(props: VirtualProps<T>) {
|
|
467
|
+
return createVirtual(lngp.Column, props, {
|
|
468
|
+
onUp: lngp.chainFunctions(props.onUp, lngp.handleNavigation('up')) as lng.KeyHandler,
|
|
469
|
+
onDown: lngp.chainFunctions(props.onDown, lngp.handleNavigation('down')) as lng.KeyHandler,
|
|
470
|
+
});
|
|
471
|
+
}
|