@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.
Files changed (41) hide show
  1. package/dist/src/primitives/Column.d.ts +4 -0
  2. package/dist/src/primitives/Column.jsx +23 -0
  3. package/dist/src/primitives/Column.jsx.map +1 -0
  4. package/dist/src/primitives/Row.d.ts +4 -0
  5. package/dist/src/primitives/Row.jsx +22 -0
  6. package/dist/src/primitives/Row.jsx.map +1 -0
  7. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  8. package/dist/src/primitives/announcer/announcer.js +4 -3
  9. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  10. package/dist/src/primitives/index.d.ts +7 -0
  11. package/dist/src/primitives/index.js +6 -0
  12. package/dist/src/primitives/index.js.map +1 -1
  13. package/dist/src/primitives/types.d.ts +60 -0
  14. package/dist/src/primitives/types.js +2 -0
  15. package/dist/src/primitives/types.js.map +1 -0
  16. package/dist/src/primitives/utils/chainFunctions.d.ts +4 -0
  17. package/dist/src/primitives/utils/chainFunctions.js +21 -0
  18. package/dist/src/primitives/utils/chainFunctions.js.map +1 -0
  19. package/dist/src/primitives/utils/createSpriteMap.d.ts +9 -0
  20. package/dist/src/primitives/utils/createSpriteMap.js +18 -0
  21. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -0
  22. package/dist/src/primitives/utils/handleNavigation.d.ts +6 -0
  23. package/dist/src/primitives/utils/handleNavigation.js +79 -0
  24. package/dist/src/primitives/utils/handleNavigation.js.map +1 -0
  25. package/dist/src/primitives/utils/scrollToIndex.d.ts +2 -0
  26. package/dist/src/primitives/utils/scrollToIndex.js +33 -0
  27. package/dist/src/primitives/utils/scrollToIndex.js.map +1 -0
  28. package/dist/src/primitives/utils/withScrolling.d.ts +9 -0
  29. package/dist/src/primitives/utils/withScrolling.js +106 -0
  30. package/dist/src/primitives/utils/withScrolling.js.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +16 -16
  33. package/src/primitives/Column.tsx +51 -0
  34. package/src/primitives/Row.tsx +51 -0
  35. package/src/primitives/announcer/announcer.ts +6 -4
  36. package/src/primitives/index.ts +8 -0
  37. package/src/primitives/types.ts +79 -0
  38. package/src/primitives/utils/chainFunctions.ts +29 -0
  39. package/src/primitives/utils/createSpriteMap.ts +32 -0
  40. package/src/primitives/utils/handleNavigation.ts +100 -0
  41. 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
+ }