@lightningtv/solid 3.0.0-8 → 3.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 (232) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +6 -0
  3. package/dist/jsx-runtime.d.ts +14 -0
  4. package/dist/src/activeElement.d.ts +1 -1
  5. package/dist/src/core/animation.d.ts +35 -0
  6. package/dist/src/core/animation.js +119 -0
  7. package/dist/src/core/animation.js.map +1 -0
  8. package/dist/src/core/config.d.ts +49 -0
  9. package/dist/src/core/config.js +33 -0
  10. package/dist/src/core/config.js.map +1 -0
  11. package/dist/src/core/domRenderer.d.ts +115 -0
  12. package/dist/src/core/domRenderer.js +1152 -0
  13. package/dist/src/core/domRenderer.js.map +1 -0
  14. package/dist/src/core/elementNode.d.ts +463 -0
  15. package/dist/src/core/elementNode.js +833 -0
  16. package/dist/src/core/elementNode.js.map +1 -0
  17. package/dist/src/core/flex.d.ts +2 -0
  18. package/dist/src/core/flex.js +243 -0
  19. package/dist/src/core/flex.js.map +1 -0
  20. package/dist/src/core/focusKeyTypes.d.ts +42 -0
  21. package/dist/src/core/focusKeyTypes.js +2 -0
  22. package/dist/src/core/focusKeyTypes.js.map +1 -0
  23. package/dist/src/core/focusManager.d.ts +13 -0
  24. package/dist/src/core/focusManager.js +276 -0
  25. package/dist/src/core/focusManager.js.map +1 -0
  26. package/dist/src/core/index.d.ts +12 -0
  27. package/dist/src/core/index.js +12 -0
  28. package/dist/src/core/index.js.map +1 -0
  29. package/dist/src/core/intrinsicTypes.d.ts +90 -0
  30. package/dist/src/core/intrinsicTypes.js +2 -0
  31. package/dist/src/core/intrinsicTypes.js.map +1 -0
  32. package/dist/src/core/lightningInit.d.ts +89 -0
  33. package/dist/src/core/lightningInit.js +26 -0
  34. package/dist/src/core/lightningInit.js.map +1 -0
  35. package/dist/src/core/nodeTypes.d.ts +6 -0
  36. package/dist/src/core/nodeTypes.js +6 -0
  37. package/dist/src/core/nodeTypes.js.map +1 -0
  38. package/dist/src/core/shaders.d.ts +51 -0
  39. package/dist/src/core/shaders.js +446 -0
  40. package/dist/src/core/shaders.js.map +1 -0
  41. package/dist/src/core/states.d.ts +12 -0
  42. package/dist/src/core/states.js +84 -0
  43. package/dist/src/core/states.js.map +1 -0
  44. package/dist/src/core/utils.d.ts +39 -0
  45. package/dist/src/core/utils.js +164 -0
  46. package/dist/src/core/utils.js.map +1 -0
  47. package/dist/src/devtools/index.d.ts +1 -1
  48. package/dist/src/devtools/index.js +1 -1
  49. package/dist/src/devtools/index.js.map +1 -1
  50. package/dist/src/index.d.ts +3 -3
  51. package/dist/src/index.js +1 -1
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/primitives/Column.jsx +9 -10
  54. package/dist/src/primitives/Column.jsx.map +1 -1
  55. package/dist/src/primitives/FPSCounter.jsx +15 -2
  56. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  57. package/dist/src/primitives/Grid.d.ts +2 -2
  58. package/dist/src/primitives/Grid.jsx +27 -17
  59. package/dist/src/primitives/Grid.jsx.map +1 -1
  60. package/dist/src/primitives/Image.d.ts +8 -0
  61. package/dist/src/primitives/Image.jsx +24 -0
  62. package/dist/src/primitives/Image.jsx.map +1 -0
  63. package/dist/src/primitives/KeepAlive.d.ts +30 -0
  64. package/dist/src/primitives/KeepAlive.jsx +77 -0
  65. package/dist/src/primitives/KeepAlive.jsx.map +1 -0
  66. package/dist/src/primitives/Lazy.d.ts +8 -7
  67. package/dist/src/primitives/Lazy.jsx +52 -20
  68. package/dist/src/primitives/Lazy.jsx.map +1 -1
  69. package/dist/src/primitives/Marquee.jsx +2 -2
  70. package/dist/src/primitives/Marquee.jsx.map +1 -1
  71. package/dist/src/primitives/Preserve.d.ts +4 -0
  72. package/dist/src/primitives/Preserve.jsx +11 -0
  73. package/dist/src/primitives/Preserve.jsx.map +1 -0
  74. package/dist/src/primitives/Row.jsx +9 -10
  75. package/dist/src/primitives/Row.jsx.map +1 -1
  76. package/dist/src/primitives/Suspense.d.ts +22 -0
  77. package/dist/src/primitives/Suspense.jsx +33 -0
  78. package/dist/src/primitives/Suspense.jsx.map +1 -0
  79. package/dist/src/primitives/Virtual.d.ts +18 -0
  80. package/dist/src/primitives/Virtual.jsx +443 -0
  81. package/dist/src/primitives/Virtual.jsx.map +1 -0
  82. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  83. package/dist/src/primitives/VirtualGrid.jsx +160 -0
  84. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  85. package/dist/src/primitives/Visible.d.ts +0 -1
  86. package/dist/src/primitives/Visible.jsx +1 -1
  87. package/dist/src/primitives/Visible.jsx.map +1 -1
  88. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  89. package/dist/src/primitives/announcer/announcer.js +4 -4
  90. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  91. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  92. package/dist/src/primitives/announcer/speech.js +147 -27
  93. package/dist/src/primitives/announcer/speech.js.map +1 -1
  94. package/dist/src/primitives/createFocusStack.d.ts +4 -4
  95. package/dist/src/primitives/createFocusStack.jsx +15 -6
  96. package/dist/src/primitives/createFocusStack.jsx.map +1 -1
  97. package/dist/src/primitives/createTag.d.ts +8 -0
  98. package/dist/src/primitives/createTag.jsx +20 -0
  99. package/dist/src/primitives/createTag.jsx.map +1 -0
  100. package/dist/src/primitives/index.d.ts +13 -4
  101. package/dist/src/primitives/index.js +12 -3
  102. package/dist/src/primitives/index.js.map +1 -1
  103. package/dist/src/primitives/types.d.ts +5 -2
  104. package/dist/src/primitives/useFocusManager.d.ts +2 -2
  105. package/dist/src/primitives/useFocusManager.js +2 -2
  106. package/dist/src/primitives/useFocusManager.js.map +1 -1
  107. package/dist/src/primitives/useMouse.d.ts +18 -2
  108. package/dist/src/primitives/useMouse.js +171 -47
  109. package/dist/src/primitives/useMouse.js.map +1 -1
  110. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  111. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  112. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  113. package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
  114. package/dist/src/primitives/utils/createSpriteMap.js +3 -3
  115. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  116. package/dist/src/primitives/utils/handleNavigation.d.ts +79 -5
  117. package/dist/src/primitives/utils/handleNavigation.js +241 -69
  118. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  119. package/dist/src/primitives/utils/withScrolling.d.ts +14 -2
  120. package/dist/src/primitives/utils/withScrolling.js +66 -7
  121. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  122. package/dist/src/render.d.ts +5 -4
  123. package/dist/src/render.js +5 -1
  124. package/dist/src/render.js.map +1 -1
  125. package/dist/src/shaders/Rounded.d.ts +7 -0
  126. package/dist/src/shaders/Rounded.js +88 -0
  127. package/dist/src/shaders/Rounded.js.map +1 -0
  128. package/dist/src/shaders/RoundedWithBorder.d.ts +3 -0
  129. package/dist/src/shaders/RoundedWithBorder.js +217 -0
  130. package/dist/src/shaders/RoundedWithBorder.js.map +1 -0
  131. package/dist/src/shaders/index.d.ts +4 -0
  132. package/dist/src/shaders/index.js +5 -0
  133. package/dist/src/shaders/index.js.map +1 -0
  134. package/dist/src/shaders/templates/RoundedTemplate.d.ts +12 -0
  135. package/dist/src/shaders/templates/RoundedTemplate.js +48 -0
  136. package/dist/src/shaders/templates/RoundedTemplate.js.map +1 -0
  137. package/dist/src/shaders/templates/RoundedWithBorderTemplate.d.ts +20 -0
  138. package/dist/src/shaders/templates/RoundedWithBorderTemplate.js +93 -0
  139. package/dist/src/shaders/templates/RoundedWithBorderTemplate.js.map +1 -0
  140. package/dist/src/shaders/utils.d.ts +3 -0
  141. package/dist/src/shaders/utils.js +31 -0
  142. package/dist/src/shaders/utils.js.map +1 -0
  143. package/dist/src/solidOpts.d.ts +1 -7
  144. package/dist/src/solidOpts.js +9 -1
  145. package/dist/src/solidOpts.js.map +1 -1
  146. package/dist/src/types.d.ts +1 -13
  147. package/dist/src/utils.d.ts +3 -1
  148. package/dist/src/utils.js +9 -1
  149. package/dist/src/utils.js.map +1 -1
  150. package/dist/tsconfig.tsbuildinfo +1 -1
  151. package/jsx-runtime.d.ts +2 -1
  152. package/package.json +28 -16
  153. package/src/activeElement.ts +1 -1
  154. package/src/core/animation.ts +185 -0
  155. package/src/core/config.ts +89 -0
  156. package/src/core/domRenderer.ts +1300 -0
  157. package/src/core/elementNode.ts +1458 -0
  158. package/src/core/flex.ts +284 -0
  159. package/src/core/focusKeyTypes.ts +90 -0
  160. package/src/core/focusManager.ts +381 -0
  161. package/src/core/index.ts +13 -0
  162. package/src/core/intrinsicTypes.ts +199 -0
  163. package/src/core/lightningInit.ts +147 -0
  164. package/src/core/nodeTypes.ts +6 -0
  165. package/src/core/shaders.ts +567 -0
  166. package/src/core/states.ts +91 -0
  167. package/src/core/utils.ts +222 -0
  168. package/src/devtools/index.ts +1 -1
  169. package/src/index.ts +3 -3
  170. package/src/primitives/Column.tsx +10 -12
  171. package/src/primitives/FPSCounter.tsx +16 -2
  172. package/src/primitives/Grid.tsx +32 -22
  173. package/src/primitives/Image.tsx +36 -0
  174. package/src/primitives/KeepAlive.tsx +124 -0
  175. package/src/primitives/Lazy.tsx +66 -37
  176. package/src/primitives/{marquee.tsx → Marquee.tsx} +1 -1
  177. package/src/primitives/Preserve.tsx +18 -0
  178. package/src/primitives/Row.tsx +13 -14
  179. package/src/primitives/Suspense.tsx +39 -0
  180. package/src/primitives/Virtual.tsx +486 -0
  181. package/src/primitives/VirtualGrid.tsx +220 -0
  182. package/src/primitives/Visible.tsx +1 -2
  183. package/src/primitives/announcer/announcer.ts +10 -4
  184. package/src/primitives/announcer/speech.ts +170 -26
  185. package/src/primitives/createFocusStack.tsx +18 -7
  186. package/src/primitives/createTag.tsx +33 -0
  187. package/src/primitives/index.ts +13 -4
  188. package/src/primitives/types.ts +12 -2
  189. package/src/primitives/useFocusManager.ts +3 -3
  190. package/src/primitives/useHold.ts +69 -0
  191. package/src/primitives/useMouse.ts +306 -67
  192. package/src/primitives/utils/createBlurredImage.ts +366 -0
  193. package/src/primitives/utils/createSpriteMap.ts +8 -6
  194. package/src/primitives/utils/handleNavigation.ts +300 -84
  195. package/src/primitives/utils/withScrolling.ts +91 -18
  196. package/src/render.ts +7 -3
  197. package/src/shaders/Rounded.ts +100 -0
  198. package/src/shaders/RoundedWithBorder.ts +245 -0
  199. package/src/shaders/index.ts +4 -0
  200. package/src/shaders/templates/RoundedTemplate.ts +57 -0
  201. package/src/shaders/templates/RoundedWithBorderTemplate.ts +110 -0
  202. package/src/shaders/utils.ts +44 -0
  203. package/src/solidOpts.ts +9 -7
  204. package/src/types.ts +1 -15
  205. package/src/utils.ts +11 -1
  206. package/dist/src/client.d.ts +0 -1
  207. package/dist/src/client.js +0 -2
  208. package/dist/src/client.js.map +0 -1
  209. package/dist/src/core.d.ts +0 -1
  210. package/dist/src/core.js +0 -3
  211. package/dist/src/core.js.map +0 -1
  212. package/dist/src/jsx-runtime.d.ts +0 -10
  213. package/dist/src/jsx-runtime.js +0 -2
  214. package/dist/src/jsx-runtime.js.map +0 -1
  215. package/dist/src/primitives/Infinite.d.ts +0 -15
  216. package/dist/src/primitives/Infinite.jsx +0 -59
  217. package/dist/src/primitives/Infinite.jsx.map +0 -1
  218. package/dist/src/primitives/LazyUp.d.ts +0 -11
  219. package/dist/src/primitives/LazyUp.jsx +0 -38
  220. package/dist/src/primitives/LazyUp.jsx.map +0 -1
  221. package/dist/src/primitives/sprite.d.ts +0 -9
  222. package/dist/src/primitives/sprite.js +0 -18
  223. package/dist/src/primitives/sprite.js.map +0 -1
  224. package/dist/src/primitives/utils/createFocusStack.d.ts +0 -24
  225. package/dist/src/primitives/utils/createFocusStack.js +0 -59
  226. package/dist/src/primitives/utils/createFocusStack.js.map +0 -1
  227. package/dist/src/primitives/utils/scrollToIndex.d.ts +0 -2
  228. package/dist/src/primitives/utils/scrollToIndex.js +0 -33
  229. package/dist/src/primitives/utils/scrollToIndex.js.map +0 -1
  230. package/dist/src/renderClient.d.ts +0 -21
  231. package/dist/src/renderClient.js +0 -64
  232. package/dist/src/renderClient.js.map +0 -1
@@ -0,0 +1,486 @@
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
+ // ScrollToIndex was called
189
+ if (Math.abs(c - prev.start) > 1) {
190
+ start = c;
191
+ if (c === 0) {
192
+ atStart = true;
193
+ selected = 0;
194
+ }
195
+ } else {
196
+ start = prev.start;
197
+ selected = prev.selected;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ break;
203
+
204
+ case 'edge':
205
+ const startScrolling = Math.max(1, props.displaySize + (atStart ? -1 : 0));
206
+ if (props.wrap) {
207
+ if (delta > 0) {
208
+ if (prev.selected < startScrolling) {
209
+ selected = prev.selected + 1;
210
+ shiftBy = 0;
211
+ } else if (prev.selected === startScrolling && atStart) {
212
+ selected = prev.selected + 1;
213
+ atStart = false;
214
+ } else {
215
+ start = utils.mod(prev.start + 1, total);
216
+ selected = prev.selected;
217
+ }
218
+ } else if (delta < 0) {
219
+ if (prev.selected > 1) {
220
+ selected = prev.selected - 1;
221
+ shiftBy = 0;
222
+ } else {
223
+ start = utils.mod(prev.start - 1, total);
224
+ selected = 1;
225
+ }
226
+ } else {
227
+ start = utils.mod(c - 1, total);
228
+ selected = 1;
229
+ shiftBy = -1;
230
+ atStart = false;
231
+ }
232
+ } else {
233
+ if (delta === 0 && c > 0) {
234
+ //initial setup
235
+ selected = c > startScrolling ? startScrolling : c;
236
+ start = Math.max(0, c - startScrolling + 1);
237
+ shiftBy = c > startScrolling ? -1 : 0;
238
+ atStart = c < startScrolling;
239
+ } else if (delta > 0) {
240
+ if (prev.selected < startScrolling) {
241
+ selected = prev.selected + 1;
242
+ shiftBy = 0;
243
+ } else if (prev.selected === startScrolling && atStart) {
244
+ selected = prev.selected + 1;
245
+ atStart = false;
246
+ } else {
247
+ start = prev.start + 1;
248
+ selected = prev.selected;
249
+ atStart = false;
250
+ }
251
+ } else if (delta < 0) {
252
+ if (prev.selected > 1) {
253
+ selected = prev.selected - 1;
254
+ shiftBy = 0;
255
+ } else if (c > 1) {
256
+ start = Math.max(0, c - 1);
257
+ selected = 1;
258
+ } else if (c === 1) {
259
+ start = 0;
260
+ selected = 1;
261
+ } else {
262
+ start = 0;
263
+ selected = 0;
264
+ shiftBy = atStart ? 0 : shiftBy;
265
+ atStart = true;
266
+ }
267
+ }
268
+ }
269
+ break;
270
+
271
+ case 'none':
272
+ default:
273
+ start = 0;
274
+ selected = c;
275
+ shiftBy = 0;
276
+ break;
277
+ }
278
+
279
+ let newSlice = prev.slice;
280
+ if (start !== prev.start || newSlice.length === 0) {
281
+ newSlice = props.wrap
282
+ ? Array.from(
283
+ { length },
284
+ (_, i) => items()[utils.mod(start + i, total)],
285
+ ) as T[]
286
+ : items().slice(start, start + length);
287
+ }
288
+
289
+ const state: SliceState = { start, slice: newSlice, selected, delta, shiftBy, atStart };
290
+
291
+ if (props.debugInfo) {
292
+ console.log(`[Virtual]`, {
293
+ cursor: c,
294
+ delta,
295
+ start,
296
+ selected,
297
+ shiftBy,
298
+ slice: state.slice,
299
+ });
300
+ }
301
+
302
+ return state;
303
+ }
304
+
305
+ let viewRef!: lngp.NavigableElement;
306
+
307
+ function scrollToIndex(this: lng.ElementNode, index: number) {
308
+ s.untrack(() => {
309
+ if (itemCount() === 0) return;
310
+
311
+ lastNavTime = performance.now();
312
+ if (originalPosition !== undefined) {
313
+ viewRef.lng[axis] = originalPosition;
314
+ targetPosition = originalPosition;
315
+ }
316
+
317
+ if (!lng.hasFocus(viewRef)) {
318
+ // force focus as scrollToIndex is manually called
319
+ viewRef.setFocus();
320
+ }
321
+
322
+ updateSelected([utils.clamp(index, 0, itemCount() - 1)]);
323
+ });
324
+ }
325
+
326
+ let lastNavTime = 0;
327
+ function getAdaptiveDuration(duration: number = 250) {
328
+ const now = performance.now();
329
+ const delta = now - lastNavTime;
330
+ lastNavTime = now;
331
+ if (delta < duration) return delta;
332
+ return duration;
333
+ }
334
+
335
+ let originalPosition: number | undefined;
336
+ const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, _active, _lastIdx) {
337
+ let idx = _idx;
338
+ let lastIdx = _lastIdx || 0;
339
+ let active = _active;
340
+ const noChange = idx === lastIdx;
341
+ const total = itemCount();
342
+ originalPosition = originalPosition ?? elm[axis];
343
+
344
+ if (props.onSelectedChanged) {
345
+ props.onSelectedChanged.call(this as lngp.NavigableElement, idx, this as lngp.NavigableElement, active, lastIdx);
346
+ }
347
+
348
+ if (noChange) return;
349
+
350
+ const rawDelta = idx - (lastIdx ?? 0);
351
+ const windowLen =
352
+ elm?.children?.length ?? props.displaySize + bufferSize();
353
+ const delta = props.wrap
354
+ ? normalizeDeltaForWindow(rawDelta, windowLen)
355
+ : rawDelta;
356
+
357
+ setCursor(c => {
358
+ const next = c + delta;
359
+ return props.wrap
360
+ ? utils.mod(next, total)
361
+ : utils.clamp(next, 0, total - 1);
362
+ });
363
+
364
+ const newState = computeSlice(cursor(), delta, slice());
365
+ setSlice(newState);
366
+ elm.selected = newState.selected;
367
+
368
+ if (
369
+ props.onEndReachedThreshold !== undefined &&
370
+ cursor() >= itemCount() - props.onEndReachedThreshold
371
+ ) {
372
+ props.onEndReached?.();
373
+ }
374
+
375
+ if (newState.shiftBy === 0) return;
376
+
377
+ const prevChildPos = (targetPosition ?? this[axis]) + active[axis];
378
+
379
+ queueMicrotask(() => {
380
+ elm.updateLayout();
381
+ const childSize = computeSize(slice().selected);
382
+
383
+ if (cachedAnimationController && cachedAnimationController.state === 'running') {
384
+ cachedAnimationController.stop();;
385
+ }
386
+
387
+ if (lng.Config.animationsEnabled) {
388
+ this.lng[axis] = prevChildPos - active[axis];
389
+ targetPosition = this.lng[axis] + (childSize * slice().shiftBy);
390
+ cachedAnimationController = this.animate(
391
+ { [axis]: targetPosition },
392
+ { ...this.animationSettings, duration: getAdaptiveDuration(this.animationSettings?.duration)}
393
+ ).start();
394
+ } else {
395
+ this.lng[axis] = this.lng[axis]! + (childSize * slice().shiftBy);
396
+ }
397
+ });
398
+ };
399
+
400
+ const updateSelected = ([sel, _items]: [number?, any?]) => {
401
+ if (!viewRef || sel === undefined || itemCount() === 0) return;
402
+ const item = items()[sel];
403
+ setCursor(sel);
404
+ const newState = computeSlice(cursor(), 0, slice());
405
+ setSlice(newState);
406
+
407
+ queueMicrotask(() => {
408
+ viewRef.updateLayout();
409
+ let activeIndex = viewRef.children.findIndex(x => x.item === item);
410
+ if (activeIndex === -1) return;
411
+ viewRef.selected = activeIndex;
412
+ if (lng.hasFocus(viewRef)) {
413
+ viewRef.children[activeIndex]?.setFocus();
414
+ }
415
+ });
416
+ };
417
+
418
+ let doOnce = false;
419
+ s.createEffect(s.on([() => props.wrap, items], () => {
420
+ if (!viewRef || itemCount() === 0 || !props.wrap || doOnce) return;
421
+ doOnce = true;
422
+ // offset just for wrap so we keep one item before
423
+ queueMicrotask(() => {
424
+ const childSize = computeSize(slice().selected);
425
+ viewRef.lng[axis] = (viewRef.lng[axis] || 0) + (childSize * -1);
426
+ // Original Position is offset to support scrollToIndex
427
+ originalPosition = viewRef.lng[axis];
428
+ targetPosition = viewRef.lng[axis];
429
+ });
430
+ }));
431
+
432
+ s.createEffect(s.on([() => props.selected, items], updateSelected));
433
+
434
+ s.createEffect(s.on(items, () => {
435
+ if (!viewRef) return;
436
+ if (cursor() >= itemCount()) {
437
+ setCursor(Math.max(0, itemCount() - 1));
438
+ }
439
+ const newState = computeSlice(cursor(), 0, slice());
440
+ setSlice(newState);
441
+ viewRef.selected = newState.selected;
442
+ }));
443
+
444
+ return (<view
445
+ {...props}
446
+ {...keyHandlers}
447
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
448
+ selected={selected()}
449
+ cursor={cursor()}
450
+ forwardFocus={/* @once */ lngp.navigableForwardFocus}
451
+ scrollToIndex={/* @once */ scrollToIndex}
452
+ onSelectedChanged={/* @once */ onSelectedChanged}
453
+ style={/* @once */ lng.combineStyles(
454
+ props.style,
455
+ component === lngp.Row
456
+ ? {
457
+ display: 'flex',
458
+ gap: 30,
459
+ transition: { x: { duration: 250, easing: 'ease-out' } },
460
+ }
461
+ : {
462
+ display: 'flex',
463
+ flexDirection: 'column',
464
+ gap: 30,
465
+ transition: { y: { duration: 250, easing: 'ease-out' } },
466
+ }
467
+ )}
468
+ >
469
+ <List each={slice().slice}>{props.children}</List>
470
+ </view>
471
+ );
472
+ }
473
+
474
+ export function VirtualRow<T>(props: VirtualProps<T>) {
475
+ return createVirtual(lngp.Row, props, {
476
+ onLeft: lngp.chainFunctions(props.onLeft, lngp.handleNavigation('left')) as lng.KeyHandler,
477
+ onRight: lngp.chainFunctions(props.onRight, lngp.handleNavigation('right')) as lng.KeyHandler,
478
+ });
479
+ }
480
+
481
+ export function VirtualColumn<T>(props: VirtualProps<T>) {
482
+ return createVirtual(lngp.Column, props, {
483
+ onUp: lngp.chainFunctions(props.onUp, lngp.handleNavigation('up')) as lng.KeyHandler,
484
+ onDown: lngp.chainFunctions(props.onDown, lngp.handleNavigation('down')) as lng.KeyHandler,
485
+ });
486
+ }
@@ -0,0 +1,220 @@
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
+ const columnScroll = lngp.withScrolling(false);
8
+
9
+ const rowStyles: lng.NodeStyles = {
10
+ display: 'flex',
11
+ flexWrap: 'wrap',
12
+ transition: {
13
+ y: true,
14
+ },
15
+ };
16
+
17
+ export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
18
+ each: readonly T[] | undefined | null | false;
19
+ columns: number; // items per row
20
+ rows?: number; // number of visible rows (default: 1)
21
+ buffer?: number;
22
+ onEndReached?: () => void;
23
+ onEndReachedThreshold?: number;
24
+ children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
25
+ };
26
+
27
+ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
28
+ const bufferSize = () => props.buffer ?? 2;
29
+ const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
30
+ const items = s.createMemo(() => props.each || []);
31
+ const itemCount = () => items().length;
32
+ const itemsPerRow = () => props.columns;
33
+ const numberOfRows = () => props.rows ?? 1;
34
+ const totalVisibleItems = () => itemsPerRow() * numberOfRows();
35
+
36
+ const start = s.createMemo(() => {
37
+ const perRow = itemsPerRow();
38
+ const newRowIndex = Math.floor(cursor() / perRow);
39
+ const rawStart = newRowIndex * perRow - bufferSize() * perRow;
40
+ return Math.max(0, rawStart);
41
+ });
42
+
43
+ const end = s.createMemo(() => {
44
+ const perRow = itemsPerRow();
45
+ const newRowIndex = Math.floor(cursor() / perRow);
46
+ const rawEnd = (newRowIndex + bufferSize()) * perRow + totalVisibleItems();
47
+ return Math.min(items().length, rawEnd);
48
+ });
49
+
50
+ const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
51
+
52
+ let viewRef!: lngp.NavigableElement;
53
+
54
+ function onVerticalNav(dir: -1 | 1): lngp.KeyHandler {
55
+ return function () {
56
+ const perRow = itemsPerRow();
57
+ const currentRowIndex = Math.floor(cursor() / perRow);
58
+ const maxRows = Math.floor(items().length / perRow);
59
+
60
+ if (
61
+ currentRowIndex === 0 && dir === -1
62
+ || currentRowIndex === maxRows && dir === 1
63
+ ) return;
64
+
65
+ const selected = this.selected || 0;
66
+ const offset = dir * perRow;
67
+ const newIndex = utils.clamp(selected + offset, 0, items().length - 1);
68
+ const lastIdx = selected;
69
+ this.selected = newIndex;
70
+ const active = this.children[this.selected];
71
+
72
+ if (active instanceof lng.ElementNode) {
73
+ active.setFocus();
74
+ chainedOnSelectedChanged.call(
75
+ this as lngp.NavigableElement,
76
+ this.selected,
77
+ this as lngp.NavigableElement,
78
+ active,
79
+ lastIdx
80
+ );
81
+ return true;
82
+ }
83
+ };
84
+ }
85
+
86
+ const onUp = onVerticalNav(-1);
87
+ const onDown = onVerticalNav(1);
88
+
89
+ const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
90
+ let idx = _idx;
91
+ let lastIdx = _lastIdx;
92
+ const perRow = itemsPerRow();
93
+ const newRowIndex = Math.floor(idx / perRow);
94
+ const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
95
+ const prevStart = start();
96
+
97
+ setCursor(prevStart + idx);
98
+ if (newRowIndex === prevRowIndex) return;
99
+
100
+ setSlice(items().slice(start(), end()));
101
+
102
+ // this.selected is relative to the slice
103
+ // and it doesn't get corrected automatically after children change
104
+ const idxCorrection = prevStart - start();
105
+ if (lastIdx) lastIdx += idxCorrection;
106
+ idx += idxCorrection;
107
+ this.selected += idxCorrection;
108
+
109
+ if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
110
+ props.onEndReached?.();
111
+ }
112
+
113
+ queueMicrotask(() => {
114
+ const prevRowY = this.y + active.y;
115
+ this.updateLayout();
116
+ this.lng.y = prevRowY - active.y;
117
+ columnScroll(idx, elm, active, lastIdx);
118
+ });
119
+ };
120
+
121
+ const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
122
+
123
+ let cachedSelected: number | undefined;
124
+ const updateSelected = ([selected, _items]: [number?, any?]) => {
125
+ if (!viewRef || selected == null) return;
126
+
127
+ if (cachedSelected !== undefined) {
128
+ selected = cachedSelected;
129
+ cachedSelected = undefined;
130
+ }
131
+
132
+ if (selected >= items().length && props.onEndReached) {
133
+ props.onEndReached?.();
134
+ cachedSelected = selected;
135
+ return;
136
+ }
137
+
138
+ const item = items()[selected];
139
+ let active = viewRef.children.find(x => x.item === item);
140
+ const lastSelected = viewRef.selected;
141
+
142
+ if (active instanceof lng.ElementNode) {
143
+ viewRef.selected = viewRef.children.indexOf(active);
144
+ if (lng.hasFocus(viewRef)) {
145
+ // force focus as scrollToIndex is manually called
146
+ active.setFocus();
147
+ }
148
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
149
+ } else {
150
+ setCursor(selected);
151
+ setSlice(items().slice(start(), end()));
152
+
153
+ queueMicrotask(() => {
154
+ viewRef.updateLayout();
155
+ active = viewRef.children.find(x => x.item === item);
156
+ if (active instanceof lng.ElementNode) {
157
+ viewRef.selected = viewRef.children.indexOf(active);
158
+ if (lng.hasFocus(viewRef)) {
159
+ active.setFocus();
160
+ }
161
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
162
+ }
163
+ });
164
+ }
165
+ };
166
+
167
+ const scrollToIndex = (index: number) => {
168
+ s.untrack(() => updateSelected([index]));
169
+ }
170
+
171
+ s.createEffect(s.on([() => props.selected, items], updateSelected));
172
+
173
+ s.createEffect(
174
+ s.on(items, (gridItems, _prevGridItems, prevSize) => {
175
+ if (!viewRef) return;
176
+
177
+ if (cachedSelected !== undefined) {
178
+ // This occurs when VG is reloaded and user wants to select a paginated item
179
+ updateSelected([cachedSelected]);
180
+ return gridItems.length;
181
+ }
182
+
183
+ if (gridItems.length === 0) {
184
+ setCursor(0);
185
+ cachedSelected = undefined;
186
+ setSlice([]);
187
+ } else if (cursor() >= itemCount()) {
188
+ updateSelected([Math.max(0, itemCount() - 1)]);
189
+ } else if (prevSize === 0) {
190
+ updateSelected([0]);
191
+ } else {
192
+ setSlice(items().slice(start(), end()));
193
+ }
194
+
195
+ return gridItems.length;
196
+ }, { defer: true })
197
+ );
198
+
199
+
200
+ return (
201
+ <view
202
+ {...props}
203
+ scroll={props.scroll || 'always'}
204
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
205
+ selected={props.selected || 0}
206
+ cursor={cursor()}
207
+ onLeft={/* @once */ lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation)}
208
+ onRight={/* @once */ lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation)}
209
+ onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
210
+ onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
211
+ forwardFocus={/* @once */ lngp.navigableForwardFocus}
212
+ onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
213
+ scrollToIndex={/* @once */ scrollToIndex}
214
+ onSelectedChanged={/* @once */ chainedOnSelectedChanged}
215
+ style={/* @once */ lng.combineStyles(props.style, rowStyles)}
216
+ >
217
+ <List each={slice()}>{props.children}</List>
218
+ </view>
219
+ );
220
+ }