@lightningtv/solid 2.4.4 → 2.4.6
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/dist/src/primitives/Column.d.ts +4 -0
- package/dist/src/primitives/Column.jsx +23 -0
- package/dist/src/primitives/Column.jsx.map +1 -0
- package/dist/src/primitives/Row.d.ts +4 -0
- package/dist/src/primitives/Row.jsx +22 -0
- package/dist/src/primitives/Row.jsx.map +1 -0
- package/dist/src/primitives/announcer/announcer.d.ts +1 -0
- package/dist/src/primitives/announcer/announcer.js +4 -3
- package/dist/src/primitives/announcer/announcer.js.map +1 -1
- package/dist/src/primitives/index.d.ts +7 -0
- package/dist/src/primitives/index.js +6 -0
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +60 -0
- package/dist/src/primitives/types.js +2 -0
- package/dist/src/primitives/types.js.map +1 -0
- package/dist/src/primitives/utils/chainFunctions.d.ts +4 -0
- package/dist/src/primitives/utils/chainFunctions.js +21 -0
- package/dist/src/primitives/utils/chainFunctions.js.map +1 -0
- package/dist/src/primitives/utils/createSpriteMap.d.ts +9 -0
- package/dist/src/primitives/utils/createSpriteMap.js +18 -0
- package/dist/src/primitives/utils/createSpriteMap.js.map +1 -0
- package/dist/src/primitives/utils/handleNavigation.d.ts +6 -0
- package/dist/src/primitives/utils/handleNavigation.js +79 -0
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -0
- package/dist/src/primitives/utils/scrollToIndex.d.ts +2 -0
- package/dist/src/primitives/utils/scrollToIndex.js +33 -0
- package/dist/src/primitives/utils/scrollToIndex.js.map +1 -0
- package/dist/src/primitives/utils/withScrolling.d.ts +9 -0
- package/dist/src/primitives/utils/withScrolling.js +106 -0
- package/dist/src/primitives/utils/withScrolling.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/primitives/Column.tsx +51 -0
- package/src/primitives/Row.tsx +51 -0
- package/src/primitives/announcer/announcer.ts +6 -4
- package/src/primitives/index.ts +8 -0
- package/src/primitives/types.ts +79 -0
- package/src/primitives/utils/chainFunctions.ts +29 -0
- package/src/primitives/utils/createSpriteMap.ts +32 -0
- package/src/primitives/utils/handleNavigation.ts +100 -0
- package/src/primitives/utils/withScrolling.ts +136 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
|
|
2
|
+
import type { KeyHandler } from '@lightningtv/core/focusManager';
|
|
3
|
+
export type OnSelectedChanged = (
|
|
4
|
+
this: NavigableElement,
|
|
5
|
+
selectedIndex: number,
|
|
6
|
+
elm: NavigableElement,
|
|
7
|
+
active: ElementNode,
|
|
8
|
+
lastSelectedIndex?: number,
|
|
9
|
+
) => void;
|
|
10
|
+
export interface NavigableProps extends NodeProps {
|
|
11
|
+
/** function to be called when the selected of the component changes */
|
|
12
|
+
onSelectedChanged?: OnSelectedChanged;
|
|
13
|
+
|
|
14
|
+
/** Determines when to scroll(shift items along the axis):
|
|
15
|
+
* auto - scroll items immediately
|
|
16
|
+
* edge - scroll items when focus reaches the last item on screen
|
|
17
|
+
* always - focus remains at index 0, scroll until the final item is at index 0
|
|
18
|
+
* center - selected element will be centered to the screen
|
|
19
|
+
* none - disable scrolling behavior, focus shifts as expected
|
|
20
|
+
* in both `auto` and `edge` items will only scroll until the last item is on screen */
|
|
21
|
+
scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center';
|
|
22
|
+
|
|
23
|
+
/** When auto scrolling, item index at which scrolling begins */
|
|
24
|
+
scrollIndex?: number;
|
|
25
|
+
|
|
26
|
+
/** The initial index */
|
|
27
|
+
selected?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adjust the x position of the row. Initial value is Y
|
|
31
|
+
*/
|
|
32
|
+
offset?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Plinko - sets the selected item of the next row to match the previous row
|
|
36
|
+
*/
|
|
37
|
+
plinko?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wrap the row so active goes back to the beginning of the row
|
|
41
|
+
*/
|
|
42
|
+
wrap?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// @ts-expect-error animationSettings is not identical - weird
|
|
46
|
+
export interface NavigableElement extends ElementNode, NavigableProps {
|
|
47
|
+
selected: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NavigableStyleProperties {
|
|
51
|
+
/**
|
|
52
|
+
* the index of which we want scrolling to start
|
|
53
|
+
*/
|
|
54
|
+
scrollIndex?: number;
|
|
55
|
+
/**
|
|
56
|
+
* space between each keys
|
|
57
|
+
*/
|
|
58
|
+
itemSpacing?: NodeStyles['gap'];
|
|
59
|
+
/**
|
|
60
|
+
* animation transition
|
|
61
|
+
*/
|
|
62
|
+
itemTransition?: NodeStyles['transition'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ColumnProps extends NavigableProps, NavigableStyleProperties {
|
|
66
|
+
/** function to be called on down click */
|
|
67
|
+
onDown?: KeyHandler;
|
|
68
|
+
|
|
69
|
+
/** function to be called on up click */
|
|
70
|
+
onUp?: KeyHandler;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RowProps extends NavigableProps, NavigableStyleProperties {
|
|
74
|
+
/** function to be called on down click */
|
|
75
|
+
onLeft?: KeyHandler;
|
|
76
|
+
|
|
77
|
+
/** function to be called on up click */
|
|
78
|
+
onRight?: KeyHandler;
|
|
79
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type ChainableFunction = (...args: unknown[]) => unknown;
|
|
2
|
+
|
|
3
|
+
export function chainFunctions(...args: ChainableFunction[]): ChainableFunction;
|
|
4
|
+
export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
|
|
5
|
+
|
|
6
|
+
// take an array of functions and if you return true from a function, it will stop the chain
|
|
7
|
+
export function chainFunctions<T extends ChainableFunction>(
|
|
8
|
+
...args: (ChainableFunction | T)[]
|
|
9
|
+
) {
|
|
10
|
+
const onlyFunctions = args.filter((func) => typeof func === 'function');
|
|
11
|
+
if (onlyFunctions.length === 0) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (onlyFunctions.length === 1) {
|
|
16
|
+
return onlyFunctions[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return function (this: unknown | T, ...innerArgs: unknown[]) {
|
|
20
|
+
let result;
|
|
21
|
+
for (const func of onlyFunctions) {
|
|
22
|
+
result = func.apply(this, innerArgs);
|
|
23
|
+
if (result === true) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { renderer, type TextureMap } from '@lightningtv/core';
|
|
2
|
+
|
|
3
|
+
export interface SpriteDef {
|
|
4
|
+
name: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createSpriteMap(
|
|
12
|
+
src: string,
|
|
13
|
+
subTextures: SpriteDef[],
|
|
14
|
+
): Record<string, InstanceType<TextureMap['SubTexture']>> {
|
|
15
|
+
const spriteMapTexture = renderer.createTexture('ImageTexture', {
|
|
16
|
+
src,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return subTextures.reduce<
|
|
20
|
+
Record<string, InstanceType<TextureMap['SubTexture']>>
|
|
21
|
+
>((acc, t) => {
|
|
22
|
+
const { x, y, width, height } = t;
|
|
23
|
+
acc[t.name] = renderer.createTexture('SubTexture', {
|
|
24
|
+
texture: spriteMapTexture,
|
|
25
|
+
x,
|
|
26
|
+
y,
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
});
|
|
30
|
+
return acc;
|
|
31
|
+
}, {});
|
|
32
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ElementNode, assertTruthy } from '@lightningtv/core';
|
|
2
|
+
import { type KeyHandler } from '@lightningtv/core/focusManager';
|
|
3
|
+
import type { NavigableElement, OnSelectedChanged } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export function onGridFocus(this: ElementNode) {
|
|
6
|
+
if (!this || this.children.length === 0) return false;
|
|
7
|
+
|
|
8
|
+
this.selected = this.selected || 0;
|
|
9
|
+
let child = this.selected ? this.children[this.selected] : this.selectedNode;
|
|
10
|
+
|
|
11
|
+
while (child?.skipFocus) {
|
|
12
|
+
this.selected++;
|
|
13
|
+
child = this.children[this.selected];
|
|
14
|
+
}
|
|
15
|
+
if (!(child instanceof ElementNode)) return false;
|
|
16
|
+
child.setFocus();
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Converts params from onFocus to onSelectedChanged
|
|
21
|
+
export function handleOnSelect(onSelectedChanged: OnSelectedChanged) {
|
|
22
|
+
return function (this: NavigableElement) {
|
|
23
|
+
return onSelectedChanged.call(
|
|
24
|
+
this,
|
|
25
|
+
this.selected,
|
|
26
|
+
this,
|
|
27
|
+
this.children[this.selected] as ElementNode,
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function handleNavigation(
|
|
33
|
+
direction: 'up' | 'right' | 'down' | 'left',
|
|
34
|
+
): KeyHandler {
|
|
35
|
+
return function () {
|
|
36
|
+
const numChildren = this.children.length;
|
|
37
|
+
const wrap = this.wrap;
|
|
38
|
+
const lastSelected = this.selected || 0;
|
|
39
|
+
|
|
40
|
+
if (numChildren === 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (direction === 'right' || direction === 'down') {
|
|
45
|
+
do {
|
|
46
|
+
this.selected = ((this.selected || 0) % numChildren) + 1;
|
|
47
|
+
if (this.selected >= numChildren) {
|
|
48
|
+
if (!wrap) {
|
|
49
|
+
this.selected = -1;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
this.selected = 0;
|
|
53
|
+
}
|
|
54
|
+
} while (this.children[this.selected]?.skipFocus);
|
|
55
|
+
} else if (direction === 'left' || direction === 'up') {
|
|
56
|
+
do {
|
|
57
|
+
this.selected = ((this.selected || 0) % numChildren) - 1;
|
|
58
|
+
if (this.selected < 0) {
|
|
59
|
+
if (!wrap) {
|
|
60
|
+
this.selected = -1;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
this.selected = numChildren - 1;
|
|
64
|
+
}
|
|
65
|
+
} while (this.children[this.selected]?.skipFocus);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.selected === -1) {
|
|
69
|
+
this.selected = lastSelected;
|
|
70
|
+
if (this.children[this.selected]?.states!.has('focus')) {
|
|
71
|
+
// This child is already focused, so bubble up to next handler
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const active = this.children[this.selected || 0];
|
|
76
|
+
assertTruthy(active instanceof ElementNode);
|
|
77
|
+
const navigableThis = this as NavigableElement;
|
|
78
|
+
|
|
79
|
+
navigableThis.onSelectedChanged &&
|
|
80
|
+
navigableThis.onSelectedChanged.call(
|
|
81
|
+
navigableThis,
|
|
82
|
+
navigableThis.selected,
|
|
83
|
+
navigableThis,
|
|
84
|
+
active,
|
|
85
|
+
lastSelected,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (this.plinko) {
|
|
89
|
+
// Set the next item to have the same selected index
|
|
90
|
+
// so we move up / down directly
|
|
91
|
+
const lastSelectedChild = this.children[lastSelected];
|
|
92
|
+
assertTruthy(lastSelectedChild instanceof ElementNode);
|
|
93
|
+
const num = lastSelectedChild.selected || 0;
|
|
94
|
+
active.selected =
|
|
95
|
+
num < active.children.length ? num : active.children.length - 1;
|
|
96
|
+
}
|
|
97
|
+
active.setFocus();
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ElementNode,
|
|
3
|
+
ElementText,
|
|
4
|
+
INode,
|
|
5
|
+
Styles,
|
|
6
|
+
} from '@lightningtv/core';
|
|
7
|
+
|
|
8
|
+
// Adds properties expected by withScrolling
|
|
9
|
+
export interface ScrollableElement extends ElementNode {
|
|
10
|
+
scrollIndex?: number;
|
|
11
|
+
selected: number;
|
|
12
|
+
offset?: number;
|
|
13
|
+
_targetPosition?: number;
|
|
14
|
+
_screenOffset?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// From the renderer, not exported
|
|
18
|
+
const InViewPort = 8;
|
|
19
|
+
const isNotShown = (node: ElementNode | ElementText) => {
|
|
20
|
+
return node.lng.renderState !== InViewPort;
|
|
21
|
+
};
|
|
22
|
+
/*
|
|
23
|
+
Auto Scrolling starts scrolling right away until the last item is shown. Keeping a full view of the list.
|
|
24
|
+
Edge starts scrolling when it reaches the edge of the viewport.
|
|
25
|
+
Always scroll moves the list every time
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export function withScrolling(isRow: boolean) {
|
|
29
|
+
const dimension = isRow ? 'width' : 'height';
|
|
30
|
+
const axis = isRow ? 'x' : 'y';
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
selected: number | ElementNode,
|
|
34
|
+
component?: ElementNode,
|
|
35
|
+
selectedElement?: ElementNode | ElementText,
|
|
36
|
+
lastSelected?: number,
|
|
37
|
+
) => {
|
|
38
|
+
let componentRef = component as ScrollableElement;
|
|
39
|
+
if (typeof selected !== 'number') {
|
|
40
|
+
componentRef = selected as ScrollableElement;
|
|
41
|
+
selected = componentRef.selected || 0;
|
|
42
|
+
}
|
|
43
|
+
if (!componentRef || !componentRef.children.length) return;
|
|
44
|
+
|
|
45
|
+
const lng = componentRef.lng as INode;
|
|
46
|
+
const screenSize = isRow ? lng.stage.root.width : lng.stage.root.height;
|
|
47
|
+
// Determine if movement is incremental or decremental
|
|
48
|
+
const isIncrementing =
|
|
49
|
+
lastSelected === undefined || lastSelected - 1 !== selected;
|
|
50
|
+
|
|
51
|
+
if (componentRef._screenOffset === undefined) {
|
|
52
|
+
componentRef._screenOffset =
|
|
53
|
+
componentRef.offset ??
|
|
54
|
+
(isRow ? lng.absX : lng.absY) - componentRef[axis];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const screenOffset = componentRef._screenOffset;
|
|
58
|
+
const gap = componentRef.gap || 0;
|
|
59
|
+
const scroll = componentRef.scroll || 'auto';
|
|
60
|
+
|
|
61
|
+
// Allows manual position control
|
|
62
|
+
const targetPosition = componentRef._targetPosition ?? componentRef[axis];
|
|
63
|
+
const rootPosition = isIncrementing
|
|
64
|
+
? Math.min(targetPosition, componentRef[axis])
|
|
65
|
+
: Math.max(targetPosition, componentRef[axis]);
|
|
66
|
+
componentRef.offset = componentRef.offset ?? rootPosition;
|
|
67
|
+
const offset = componentRef.offset;
|
|
68
|
+
selectedElement =
|
|
69
|
+
selectedElement || (componentRef.children[selected] as ElementNode);
|
|
70
|
+
const selectedPosition = selectedElement[axis] ?? 0;
|
|
71
|
+
const selectedSize = selectedElement[dimension] ?? 0;
|
|
72
|
+
const selectedScale =
|
|
73
|
+
selectedElement.scale ??
|
|
74
|
+
(selectedElement.style?.focus as Styles)?.scale ??
|
|
75
|
+
1;
|
|
76
|
+
const selectedSizeScaled = selectedSize * selectedScale;
|
|
77
|
+
const containerSize = componentRef[dimension] ?? 0;
|
|
78
|
+
const maxOffset = Math.min(
|
|
79
|
+
screenSize - containerSize - screenOffset - 2 * gap,
|
|
80
|
+
0,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Determine the next element based on whether incrementing or decrementing
|
|
84
|
+
const nextIndex = isIncrementing ? selected + 1 : selected - 1;
|
|
85
|
+
const nextElement = componentRef.children[nextIndex] || null;
|
|
86
|
+
|
|
87
|
+
// Default nextPosition to align with the selected position and offset
|
|
88
|
+
let nextPosition = rootPosition;
|
|
89
|
+
|
|
90
|
+
// Update nextPosition based on scroll type and specific conditions
|
|
91
|
+
if (selectedElement.centerScroll) {
|
|
92
|
+
nextPosition = -selectedPosition + (screenSize - selectedSizeScaled) / 2;
|
|
93
|
+
} else if (scroll === 'always') {
|
|
94
|
+
nextPosition = -selectedPosition + offset;
|
|
95
|
+
} else if (scroll === 'center') {
|
|
96
|
+
nextPosition =
|
|
97
|
+
-selectedPosition +
|
|
98
|
+
(screenSize - selectedSizeScaled) / 2 -
|
|
99
|
+
screenOffset;
|
|
100
|
+
} else if (!nextElement) {
|
|
101
|
+
// If at the last element, align to end
|
|
102
|
+
nextPosition = isIncrementing ? maxOffset : offset;
|
|
103
|
+
} else if (scroll === 'auto') {
|
|
104
|
+
if (
|
|
105
|
+
isIncrementing &&
|
|
106
|
+
componentRef.scrollIndex &&
|
|
107
|
+
componentRef.scrollIndex > 0 &&
|
|
108
|
+
componentRef.selected >= componentRef.scrollIndex
|
|
109
|
+
) {
|
|
110
|
+
nextPosition = rootPosition - selectedSize - gap;
|
|
111
|
+
} else if (isIncrementing) {
|
|
112
|
+
nextPosition = -selectedPosition + offset;
|
|
113
|
+
} else {
|
|
114
|
+
nextPosition = rootPosition + selectedSize + gap;
|
|
115
|
+
}
|
|
116
|
+
} // Handle Edge scrolling
|
|
117
|
+
else if (isIncrementing && isNotShown(nextElement)) {
|
|
118
|
+
nextPosition = rootPosition - selectedSize - gap;
|
|
119
|
+
} else if (isNotShown(nextElement)) {
|
|
120
|
+
nextPosition = -selectedPosition + offset;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Prevent container from moving beyond bounds
|
|
124
|
+
nextPosition =
|
|
125
|
+
isIncrementing && scroll !== 'always'
|
|
126
|
+
? Math.max(nextPosition, maxOffset)
|
|
127
|
+
: Math.min(nextPosition, offset);
|
|
128
|
+
|
|
129
|
+
// Update position if it has changed
|
|
130
|
+
if (componentRef[axis] !== nextPosition) {
|
|
131
|
+
componentRef[axis] = nextPosition;
|
|
132
|
+
// Store the new position to keep track during animations
|
|
133
|
+
componentRef._targetPosition = nextPosition;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|