@lightningtv/solid 2.10.3 → 2.10.4

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.
@@ -8,6 +8,9 @@ export type VirtualProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
8
8
  each: readonly T[] | undefined | null | false;
9
9
  displaySize: number;
10
10
  bufferSize?: number;
11
+ wrap?: boolean;
12
+ scrollIndex?: number;
13
+ doScroll?: lngp.Scroller;
11
14
  onEndReached?: () => void;
12
15
  onEndReachedThreshold?: number;
13
16
  fallback?: s.JSX.Element;
@@ -22,80 +25,153 @@ function createVirtual<T>(
22
25
  ) {
23
26
  const [cursor, setCursor] = s.createSignal(props.selected ?? 0);
24
27
  const bufferSize = s.createMemo(() => props.bufferSize || 2);
28
+ const scrollIndex = s.createMemo(() => {
29
+ return props.scrollIndex || 0;
30
+ });
25
31
  const items = s.createMemo(() => props.each || []);
32
+ const itemCount = s.createMemo(() => items().length);
33
+ const scrollType = s.createMemo(() => props.scroll || 'auto');
26
34
 
27
- const start = () =>
28
- utils.clamp(cursor() - bufferSize(), 0, Math.max(0, items().length - props.displaySize - bufferSize()));
35
+ const selected = () => {
36
+ if (props.wrap) {
37
+ return bufferSize();
38
+ }
39
+ return props.selected || 0;
40
+ };
29
41
 
30
- const end = () =>
31
- Math.min(items().length, cursor() + props.displaySize + bufferSize());
42
+ const start = () => {
43
+ if (itemCount() === 0) return 0;
44
+ if (props.wrap) {
45
+ return utils.mod(cursor() - bufferSize(), itemCount());
46
+ }
47
+ if (scrollType() === 'always') {
48
+ return Math.min(Math.max(cursor() - bufferSize(), 0), itemCount() - props.displaySize - bufferSize());
49
+ }
50
+ if (scrollType() === 'auto') {
51
+ return utils.clamp(cursor() - Math.max(bufferSize(), scrollIndex()), 0, Math.max(0, itemCount() - props.displaySize - bufferSize()));
52
+ }
53
+ return utils.clamp(cursor() - bufferSize(), 0, Math.max(0, itemCount() - props.displaySize));
54
+ };
32
55
 
33
- const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
56
+ const end = () => {
57
+ if (itemCount() === 0) return 0;
58
+ if (props.wrap) {
59
+ return (start() + props.displaySize + bufferSize()) % itemCount();
60
+ }
61
+ return Math.min(itemCount(), start() + props.displaySize + bufferSize());
62
+ };
63
+
64
+ const getSlice = s.createMemo(() => {
65
+ if (itemCount() === 0) return [];
66
+ if (!props.wrap) {
67
+ return items().slice(start(), end());
68
+ }
69
+ // Wrapping slice
70
+ const sIdx = start();
71
+ const eIdx = (sIdx + props.displaySize + bufferSize()) % itemCount();
72
+ if (sIdx < eIdx) {
73
+ return items().slice(sIdx, eIdx);
74
+ }
75
+ return [...items().slice(sIdx), ...items().slice(0, eIdx)];
76
+ });
77
+
78
+ const [slice, setSlice] = s.createSignal(getSlice());
34
79
 
35
80
  let viewRef!: lngp.NavigableElement;
36
81
 
37
82
  function scrollToIndex(this: lng.ElementNode, index: number) {
38
- updateSelected([index]);
83
+ if (itemCount() === 0) return;
84
+ let target = index;
85
+ if (props.wrap) {
86
+ target = utils.mod(index, itemCount());
87
+ } else {
88
+ target = utils.clamp(index, 0, itemCount() - 1);
89
+ }
90
+ updateSelected([target]);
39
91
  }
40
92
 
41
93
  const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx) {
42
94
  let idx = _idx;
43
- let lastIdx = _lastIdx;
95
+ let lastIdx = _lastIdx || 0;
96
+ const initialRun = idx === lastIdx;
44
97
 
45
- if (idx === lastIdx) return;
98
+ if (initialRun && !props.wrap) return;
46
99
 
47
- const prevChildPos = component === lngp.Row
48
- ? this.x + active.x
49
- : this.y + active.y;
50
-
51
- const prevStart = start();
100
+ if (!initialRun) {
101
+ if (props.wrap) {
102
+ setCursor(c => utils.mod(c + idx - lastIdx, itemCount()));
103
+ } else {
104
+ setCursor(c => utils.clamp(c + idx - lastIdx, 0, Math.max(0, itemCount() - 1)));
105
+ }
52
106
 
53
- setCursor(prevStart + idx);
54
- setSlice(items().slice(start(), end()));
107
+ setSlice(getSlice());
55
108
 
56
- const idxCorrection = prevStart - start();
57
- if (lastIdx) lastIdx += idxCorrection;
58
- idx += idxCorrection;
59
- this.selected += idxCorrection;
109
+ const c = cursor();
110
+ const scroll = scrollType();
111
+ if (props.wrap) {
112
+ this.selected = bufferSize();
113
+ } else if (props.scrollIndex) {
114
+ this.selected = Math.min(c, props.scrollIndex);
115
+ if (c >= itemCount() - props.displaySize + bufferSize()) {
116
+ this.selected = c - (itemCount() - props.displaySize) + bufferSize();
117
+ }
118
+ } else if (scroll === 'always' || scroll === 'auto') {
119
+ if (c < bufferSize()) {
120
+ this.selected = c;
121
+ } else if (c >= itemCount() - props.displaySize) {
122
+ this.selected = c - (itemCount() - props.displaySize) + bufferSize();
123
+ } else {
124
+ this.selected = bufferSize();
125
+ }
126
+ }
60
127
 
61
- if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
62
- props.onEndReached?.();
128
+ if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
129
+ props.onEndReached?.();
130
+ }
63
131
  }
132
+ const isRow = component === lngp.Row;
133
+ const prevChildPos = isRow
134
+ ? this.x + active.x
135
+ : this.y + active.y;
64
136
 
65
137
  queueMicrotask(() => {
66
138
  this.updateLayout();
67
-
139
+ // if (this._initialPosition === undefined) {
140
+ // this.offset = 0;
141
+ // const axis = isRow ? 'x' : 'y';
142
+ // this._initialPosition = this[axis];
143
+ // }
68
144
  if (component === lngp.Row) {
69
- this.lng.x = prevChildPos - active.x;
145
+ this.lng.x = this._targetPosition = prevChildPos - active.x;
70
146
  } else {
71
- this.lng.y = prevChildPos - active.y;
147
+ this.lng.y = this._targetPosition = prevChildPos - active.y;
72
148
  }
73
-
74
149
  scrollFn(idx, elm, active, lastIdx);
75
150
  });
76
151
  };
77
152
 
78
153
  const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
79
154
 
80
- const updateSelected = ([selected, _items] : [number?, any?]) => {
81
- if (!viewRef || !selected) return;
82
- const item = items()![selected];
155
+ const updateSelected = ([selected, _items]: [number?, any?]) => {
156
+ if (!viewRef || selected === undefined) return;
157
+ const sel = selected;
158
+ const item = items()[sel];
83
159
  let active = viewRef.children.find(x => x.item === item);
84
160
  const lastSelected = viewRef.selected;
85
161
 
86
162
  if (active instanceof lng.ElementNode) {
87
163
  viewRef.selected = viewRef.children.indexOf(active);
88
164
  chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
165
+ active.setFocus();
89
166
  } else {
90
- setCursor(selected);
91
- setSlice(items().slice(start(), end()));
167
+ setCursor(sel);
168
+ setSlice(getSlice());
92
169
  queueMicrotask(() => {
93
170
  viewRef.updateLayout();
94
171
  active = viewRef.children.find(x => x.item === item);
95
172
  if (active instanceof lng.ElementNode) {
96
173
  viewRef.selected = viewRef.children.indexOf(active);
97
174
  chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
98
- active.setFocus();
99
175
  }
100
176
  });
101
177
  }
@@ -105,56 +181,57 @@ function createVirtual<T>(
105
181
 
106
182
  s.createEffect(s.on(items, () => {
107
183
  if (!viewRef) return;
108
- setSlice(items().slice(start(), end()));
184
+ if (cursor() >= itemCount()) {
185
+ setCursor(Math.max(0, itemCount() - 1));
186
+ }
187
+ setSlice(getSlice());
109
188
  }, { defer: true }));
110
189
 
111
190
  return (<view
112
- {...props}
113
- scroll={props.scroll || 'always'}
114
- ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
115
- selected={props.selected || 0}
116
- cursor={cursor()}
117
- {/* @once */ ...keyHandlers}
118
- forwardFocus={/* @once */ lngp.navigableForwardFocus}
119
- scrollToIndex={/* @once */ scrollToIndex}
120
- onCreate={/* @once */
121
- props.selected
122
- ? lngp.chainFunctions(props.onCreate, scrollFn)
123
- : props.onCreate
124
- }
125
- onSelectedChanged={/* @once */ chainedOnSelectedChanged}
126
- style={/* @once */ lng.combineStyles(
127
- props.style,
128
- component === lngp.Row
129
- ? {
130
- display: 'flex',
131
- gap: 30,
132
- transition: { x: { duration: 250, easing: 'ease-out' } },
133
- }
134
- : {
135
- display: 'flex',
136
- flexDirection: 'column',
137
- gap: 30,
138
- transition: { y: { duration: 250, easing: 'ease-out' } },
139
- }
140
- )}
141
- >
142
- <List each={slice()}>{props.children}</List>
143
- </view>
144
- );
191
+ {...props}
192
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
193
+ selected={selected()}
194
+ cursor={cursor()}
195
+ {...keyHandlers}
196
+ forwardFocus={/* @once */ lngp.navigableForwardFocus}
197
+ scrollToIndex={/* @once */ scrollToIndex}
198
+ onCreate={/* @once */
199
+ props.selected
200
+ ? lngp.chainFunctions(props.onCreate, scrollFn)
201
+ : props.onCreate
202
+ }
203
+ onSelectedChanged={/* @once */ chainedOnSelectedChanged}
204
+ style={/* @once */ lng.combineStyles(
205
+ props.style,
206
+ component === lngp.Row
207
+ ? {
208
+ display: 'flex',
209
+ gap: 30,
210
+ transition: { x: { duration: 250, easing: 'ease-out' } },
211
+ }
212
+ : {
213
+ display: 'flex',
214
+ flexDirection: 'column',
215
+ gap: 30,
216
+ transition: { y: { duration: 250, easing: 'ease-out' } },
217
+ }
218
+ )}
219
+ >
220
+ <List each={slice()}>{props.children}</List>
221
+ </view>
222
+ );
145
223
  }
146
224
 
147
-
148
225
  export function VirtualRow<T>(props: VirtualProps<T>) {
149
- return createVirtual(lngp.Row, props, lngp.withScrolling(true), {
150
- onLeft: lngp.navigableHandleNavigation,
151
- onRight: lngp.navigableHandleNavigation,
226
+ return createVirtual(lngp.Row, props, props.doScroll || lngp.withScrolling(true), {
227
+ onLeft: lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation) as lng.KeyHandler,
228
+ onRight: lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation) as lng.KeyHandler,
152
229
  });
153
230
  }
154
231
 
155
232
  export function VirtualColumn<T>(props: VirtualProps<T>) {
156
- return createVirtual(lngp.Column, props, lngp.withScrolling(false), {
157
- onUp: lngp.navigableHandleNavigation,
158
- onDown: lngp.navigableHandleNavigation,
233
+ return createVirtual(lngp.Column, props, props.doScroll || lngp.withScrolling(false), {
234
+ onUp: lngp.chainFunctions(props.onUp, lngp.navigableHandleNavigation) as lng.KeyHandler,
235
+ onDown: lngp.chainFunctions(props.onDown, lngp.navigableHandleNavigation) as lng.KeyHandler,
159
236
  });
160
237
  }
@@ -164,7 +164,7 @@ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
164
164
  return (
165
165
  <view
166
166
  {...props}
167
- scroll='always'
167
+ scroll={props.scroll || 'always'}
168
168
  ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
169
169
  selected={props.selected || 0}
170
170
  cursor={cursor()}
@@ -18,7 +18,7 @@ export * from './createFocusStack.jsx';
18
18
  export * from './useHold.js';
19
19
  export * from './VirtualGrid.jsx';
20
20
  export * from './Virtual.jsx';
21
- export { withScrolling } from './utils/withScrolling.js';
21
+ export * from './utils/withScrolling.js';
22
22
  export {
23
23
  type AnyFunction,
24
24
  chainFunctions,
@@ -5,6 +5,13 @@ import type {
5
5
  Styles,
6
6
  } from '@lightningtv/core';
7
7
 
8
+ export type Scroller = (
9
+ selected: number | ElementNode,
10
+ component?: ElementNode,
11
+ selectedElement?: ElementNode | ElementText,
12
+ lastSelected?: number,
13
+ ) => void;
14
+
8
15
  // Adds properties expected by withScrolling
9
16
  export interface ScrollableElement extends ElementNode {
10
17
  scrollIndex?: number;
@@ -33,16 +40,11 @@ const isNotShown = (node: ElementNode | ElementText) => {
33
40
  Always scroll moves the list every time
34
41
  */
35
42
 
36
- export function withScrolling(isRow: boolean) {
43
+ export function withScrolling(isRow: boolean): Scroller {
37
44
  const dimension = isRow ? 'width' : 'height';
38
45
  const axis = isRow ? 'x' : 'y';
39
46
 
40
- return (
41
- selected: number | ElementNode,
42
- component?: ElementNode,
43
- selectedElement?: ElementNode | ElementText,
44
- lastSelected?: number,
45
- ) => {
47
+ return (selected, component, selectedElement, lastSelected) => {
46
48
  let componentRef = component as ScrollableElement;
47
49
  if (typeof selected !== 'number') {
48
50
  componentRef = selected as ScrollableElement;
package/src/render.ts CHANGED
@@ -156,3 +156,8 @@ export const Text = (props: TextProps) => {
156
156
  spread(el, props, false);
157
157
  return el as unknown as JSXElement;
158
158
  };
159
+
160
+ export function registerDefaultShader(name: string, shader: any) {
161
+ // noop for v2
162
+ // renderer.stage.shManager.registerShaderType('rounded', Rounded);
163
+ }
package/src/utils.ts CHANGED
@@ -66,3 +66,8 @@ export const clamp = (value: number, min: number, max: number) =>
66
66
  min < max
67
67
  ? Math.min(Math.max(value, min), max)
68
68
  : Math.min(Math.max(value, max), min);
69
+
70
+ export function mod(n: number, m: number): number {
71
+ if (m === 0) return 0;
72
+ return ((n % m) + m) % m;
73
+ }