@lightningtv/solid 3.0.0-1 → 3.0.0-11

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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/dist/src/devtools/index.d.ts +6 -0
  3. package/dist/src/devtools/index.js +65 -0
  4. package/dist/src/devtools/index.js.map +1 -0
  5. package/dist/src/index.d.ts +1 -1
  6. package/dist/src/index.js +1 -1
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/jsx-runtime.d.ts +1 -3
  9. package/dist/src/primitives/Column.jsx +2 -2
  10. package/dist/src/primitives/Column.jsx.map +1 -1
  11. package/dist/src/primitives/FPSCounter.jsx +60 -61
  12. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  13. package/dist/src/primitives/Grid.d.ts +15 -6
  14. package/dist/src/primitives/Grid.jsx +36 -23
  15. package/dist/src/primitives/Grid.jsx.map +1 -1
  16. package/dist/src/primitives/Lazy.jsx +15 -11
  17. package/dist/src/primitives/Lazy.jsx.map +1 -1
  18. package/dist/src/primitives/LazyUp.jsx +1 -0
  19. package/dist/src/primitives/LazyUp.jsx.map +1 -1
  20. package/dist/src/primitives/Marquee.d.ts +64 -0
  21. package/dist/src/primitives/Marquee.jsx +86 -0
  22. package/dist/src/primitives/Marquee.jsx.map +1 -0
  23. package/dist/src/primitives/Preserve.d.ts +4 -0
  24. package/dist/src/primitives/Preserve.jsx +11 -0
  25. package/dist/src/primitives/Preserve.jsx.map +1 -0
  26. package/dist/src/primitives/Row.jsx +2 -2
  27. package/dist/src/primitives/Row.jsx.map +1 -1
  28. package/dist/src/primitives/Suspense.d.ts +23 -0
  29. package/dist/src/primitives/Suspense.jsx +34 -0
  30. package/dist/src/primitives/Suspense.jsx.map +1 -0
  31. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  32. package/dist/src/primitives/announcer/announcer.js +6 -5
  33. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  34. package/dist/src/primitives/announcer/index.d.ts +5 -1
  35. package/dist/src/primitives/announcer/index.js +8 -2
  36. package/dist/src/primitives/announcer/index.js.map +1 -1
  37. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  38. package/dist/src/primitives/announcer/speech.js +67 -28
  39. package/dist/src/primitives/announcer/speech.js.map +1 -1
  40. package/dist/src/primitives/index.d.ts +5 -1
  41. package/dist/src/primitives/index.js +5 -1
  42. package/dist/src/primitives/index.js.map +1 -1
  43. package/dist/src/primitives/types.d.ts +2 -0
  44. package/dist/src/primitives/useHold.d.ts +27 -0
  45. package/dist/src/primitives/useHold.js +54 -0
  46. package/dist/src/primitives/useHold.js.map +1 -0
  47. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  48. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  49. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  50. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  51. package/dist/src/primitives/utils/handleNavigation.js +11 -2
  52. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  53. package/dist/src/primitives/utils/withScrolling.d.ts +3 -0
  54. package/dist/src/primitives/utils/withScrolling.js +14 -1
  55. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  56. package/dist/src/render.d.ts +5 -5
  57. package/dist/src/render.js +8 -6
  58. package/dist/src/render.js.map +1 -1
  59. package/dist/src/solidOpts.d.ts +7 -0
  60. package/dist/src/solidOpts.js +39 -7
  61. package/dist/src/solidOpts.js.map +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/{src/jsx-runtime.ts → jsx-runtime.d.ts} +3 -3
  64. package/package.json +23 -8
  65. package/src/devtools/index.ts +77 -0
  66. package/src/index.ts +1 -1
  67. package/src/primitives/Column.tsx +2 -2
  68. package/src/primitives/FPSCounter.tsx +61 -61
  69. package/src/primitives/Grid.tsx +59 -35
  70. package/src/primitives/Lazy.tsx +21 -12
  71. package/src/primitives/Marquee.tsx +149 -0
  72. package/src/primitives/Preserve.tsx +18 -0
  73. package/src/primitives/Row.tsx +1 -1
  74. package/src/primitives/Suspense.tsx +41 -0
  75. package/src/primitives/announcer/announcer.ts +9 -10
  76. package/src/primitives/announcer/index.ts +12 -2
  77. package/src/primitives/announcer/speech.ts +82 -28
  78. package/src/primitives/index.ts +9 -1
  79. package/src/primitives/types.ts +9 -0
  80. package/src/primitives/useHold.ts +69 -0
  81. package/src/primitives/utils/chainFunctions.ts +40 -9
  82. package/src/primitives/utils/createSpriteMap.ts +2 -2
  83. package/src/primitives/utils/handleNavigation.ts +11 -2
  84. package/src/primitives/utils/withScrolling.ts +25 -2
  85. package/src/render.ts +12 -12
  86. package/src/solidOpts.ts +51 -7
  87. package/src/primitives/jsx-runtime.d.ts +0 -8
@@ -1,4 +1,8 @@
1
- type CoreSpeechType = string | (() => SpeechType) | SpeechType[];
1
+ type CoreSpeechType =
2
+ | string
3
+ | (() => SpeechType)
4
+ | SpeechType[]
5
+ | SpeechSynthesisUtterance;
2
6
  export type SpeechType = CoreSpeechType | Promise<CoreSpeechType>;
3
7
 
4
8
  export interface SeriesResult {
@@ -48,11 +52,23 @@ function speak(
48
52
  phrase: string,
49
53
  utterances: SpeechSynthesisUtterance[],
50
54
  lang = 'en-US',
55
+ voiceName?: string,
51
56
  ) {
52
57
  const synth = window.speechSynthesis;
58
+
53
59
  return new Promise<void>((resolve, reject) => {
60
+ let selectedVoice;
61
+ if (voiceName) {
62
+ const availableVoices = synth.getVoices();
63
+ selectedVoice =
64
+ availableVoices.find((v) => v.name === voiceName) || availableVoices[0];
65
+ }
66
+
54
67
  const utterance = new SpeechSynthesisUtterance(phrase);
55
68
  utterance.lang = lang;
69
+ if (selectedVoice) {
70
+ utterance.voice = selectedVoice;
71
+ }
56
72
  utterance.onend = () => {
57
73
  resolve();
58
74
  };
@@ -67,6 +83,7 @@ function speak(
67
83
  function speakSeries(
68
84
  series: SpeechType,
69
85
  lang: string,
86
+ voice?: string,
70
87
  root = true,
71
88
  ): SeriesResult {
72
89
  const synth = window.speechSynthesis;
@@ -74,11 +91,6 @@ function speakSeries(
74
91
  Array.isArray(series) ? series : [series],
75
92
  );
76
93
  const nestedSeriesResults: SeriesResult[] = [];
77
- /*
78
- We hold this array of SpeechSynthesisUtterances in order to prevent them from being
79
- garbage collected prematurely on STB hardware which can cause the 'onend' events of
80
- utterances to not fire consistently.
81
- */
82
94
  const utterances: SpeechSynthesisUtterance[] = [];
83
95
  let active: boolean = true;
84
96
 
@@ -87,24 +99,61 @@ function speakSeries(
87
99
  while (active && remainingPhrases.length) {
88
100
  const phrase = await Promise.resolve(remainingPhrases.shift());
89
101
  if (!active) {
90
- // Exit
91
- // Need to check this after the await in case it was cancelled in between
92
- break;
93
- } else if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
94
- // Pause it
95
- let pause = Number(phrase.split('PAUSE-')[1]) * 1000;
96
- if (isNaN(pause)) {
97
- pause = 0;
102
+ break; // Exit if canceled
103
+ }
104
+
105
+ if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
106
+ // Handle pauses
107
+ const pause = Number(phrase.split('PAUSE-')[1]) * 1000;
108
+ if (!isNaN(pause)) {
109
+ await delay(pause);
110
+ }
111
+ } else if (typeof phrase === 'string') {
112
+ if (!phrase) {
113
+ continue; // Skip empty strings
114
+ }
115
+ // Handle regular strings with retry logic
116
+ const totalRetries = 3;
117
+ let retriesLeft = totalRetries;
118
+
119
+ while (active && retriesLeft > 0) {
120
+ try {
121
+ await speak(phrase, utterances, lang, voice);
122
+ retriesLeft = 0; // Exit retry loop on success
123
+ } catch (e) {
124
+ if (e instanceof SpeechSynthesisErrorEvent) {
125
+ if (e.error === 'network') {
126
+ retriesLeft--;
127
+ console.warn(
128
+ `Speech synthesis network error. Retries left: ${retriesLeft}`,
129
+ );
130
+ await delay(500 * (totalRetries - retriesLeft));
131
+ } else if (
132
+ e.error === 'canceled' ||
133
+ e.error === 'interrupted'
134
+ ) {
135
+ // Cancel or interrupt error (ignore)
136
+ retriesLeft = 0;
137
+ } else {
138
+ throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
139
+ }
140
+ } else {
141
+ throw e;
142
+ }
143
+ }
98
144
  }
99
- await delay(pause);
100
- } else if (typeof phrase === 'string' && phrase.length) {
101
- // Speak it
145
+ } else if (phrase instanceof SpeechSynthesisUtterance) {
146
+ // Handle SpeechSynthesisUtterance objects with retry logic
102
147
  const totalRetries = 3;
103
148
  let retriesLeft = totalRetries;
149
+ const text = phrase.text;
150
+ const objectLang = phrase?.lang;
151
+ const objectVoice = phrase?.voice;
152
+
104
153
  while (active && retriesLeft > 0) {
105
154
  try {
106
- await speak(phrase, utterances, lang);
107
- retriesLeft = 0;
155
+ await speak(text, utterances, objectLang, objectVoice?.name);
156
+ retriesLeft = 0; // Exit retry loop on success
108
157
  } catch (e) {
109
158
  if (e instanceof SpeechSynthesisErrorEvent) {
110
159
  if (e.error === 'network') {
@@ -128,12 +177,13 @@ function speakSeries(
128
177
  }
129
178
  }
130
179
  } else if (typeof phrase === 'function') {
131
- const seriesResult = speakSeries(phrase(), lang, false);
180
+ // Handle functions
181
+ const seriesResult = speakSeries(phrase(), lang, voice, false);
132
182
  nestedSeriesResults.push(seriesResult);
133
183
  await seriesResult.series;
134
184
  } else if (Array.isArray(phrase)) {
135
- // Speak it (recursively)
136
- const seriesResult = speakSeries(phrase, lang, false);
185
+ // Handle nested arrays
186
+ const seriesResult = speakSeries(phrase, lang, voice, false);
137
187
  nestedSeriesResults.push(seriesResult);
138
188
  await seriesResult.series;
139
189
  }
@@ -142,6 +192,7 @@ function speakSeries(
142
192
  active = false;
143
193
  }
144
194
  })();
195
+
145
196
  return {
146
197
  series: seriesChain,
147
198
  get active() {
@@ -155,19 +206,22 @@ function speakSeries(
155
206
  return;
156
207
  }
157
208
  if (root) {
158
- synth.cancel();
209
+ synth.cancel(); // Cancel all ongoing speech
159
210
  }
160
- nestedSeriesResults.forEach((nestedSeriesResults) => {
161
- nestedSeriesResults.cancel();
211
+ nestedSeriesResults.forEach((nestedSeriesResult) => {
212
+ nestedSeriesResult.cancel();
162
213
  });
163
214
  active = false;
164
215
  },
165
216
  };
166
217
  }
167
-
168
218
  let currentSeries: SeriesResult | undefined;
169
- export default function (toSpeak: SpeechType, lang: string = 'en-US') {
219
+ export default function (
220
+ toSpeak: SpeechType,
221
+ lang: string = 'en-US',
222
+ voice?: string,
223
+ ) {
170
224
  currentSeries && currentSeries.cancel();
171
- currentSeries = speakSeries(toSpeak, lang);
225
+ currentSeries = speakSeries(toSpeak, lang, voice);
172
226
  return currentSeries;
173
227
  }
@@ -11,9 +11,17 @@ export * from './Row.jsx';
11
11
  export * from './Grid.jsx';
12
12
  export * from './FPSCounter.jsx';
13
13
  export * from './FadeInOut.jsx';
14
+ export * from './Preserve.jsx';
15
+ export * from './Suspense.jsx';
16
+ export * from './Marquee.jsx';
14
17
  export * from './createFocusStack.jsx';
18
+ export * from './useHold.js';
15
19
  export { withScrolling } from './utils/withScrolling.js';
16
- export { chainFunctions } from './utils/chainFunctions.js';
20
+ export {
21
+ type AnyFunction,
22
+ chainFunctions,
23
+ chainRefs,
24
+ } from './utils/chainFunctions.js';
17
25
  export { handleNavigation, onGridFocus } from './utils/handleNavigation.js';
18
26
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
19
27
 
@@ -1,5 +1,6 @@
1
1
  import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
2
2
  import type { KeyHandler } from '@lightningtv/core/focusManager';
3
+
3
4
  export type OnSelectedChanged = (
4
5
  this: NavigableElement,
5
6
  selectedIndex: number,
@@ -7,6 +8,7 @@ export type OnSelectedChanged = (
7
8
  active: ElementNode,
8
9
  lastSelectedIndex?: number,
9
10
  ) => void;
11
+
10
12
  export interface NavigableProps extends NodeProps {
11
13
  /** function to be called when the selected of the component changes */
12
14
  onSelectedChanged?: OnSelectedChanged;
@@ -40,6 +42,13 @@ export interface NavigableProps extends NodeProps {
40
42
  * Wrap the row so active goes back to the beginning of the row
41
43
  */
42
44
  wrap?: boolean;
45
+
46
+ /** function to be called when scrolled */
47
+ onScrolled?: (
48
+ elm: NavigableElement,
49
+ offset: number,
50
+ isInitial: boolean,
51
+ ) => void;
43
52
  }
44
53
 
45
54
  // @ts-expect-error animationSettings is not identical - weird
@@ -0,0 +1,69 @@
1
+ import { createMemo } from 'solid-js';
2
+
3
+ export type UseHoldProps = {
4
+ onHold: () => void;
5
+ onEnter: () => void;
6
+ onRelease?: () => void;
7
+ holdThreshold?: number;
8
+ performOnEnterImmediately?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @example
13
+ * const [holdRight, releaseRight] = useHold({
14
+ * onHold: handleHoldRight,
15
+ * onEnter: handleOnRight,
16
+ * onRelease: handleReleaseHold,
17
+ * holdThreshold: 200,
18
+ * performOnEnterImmediately: true
19
+ * });
20
+ *
21
+ * <View
22
+ * onRight={holdRight}
23
+ * onRightRelease={releaseRight}
24
+ * />
25
+ *
26
+ * @param {UseHoldProps} props - The properties for configuring the hold behavior.
27
+ * @returns {[() => boolean, () => boolean]} A tuple containing `startHold` and `releaseHold` functions.
28
+ */
29
+
30
+ export function useHold(props: UseHoldProps) {
31
+ const holdThreshold = createMemo(() => props.holdThreshold ?? 500);
32
+ const performOnEnterImmediately = createMemo(
33
+ () => props.performOnEnterImmediately ?? false,
34
+ );
35
+
36
+ let holdTimeout = -1;
37
+ let wasHeld = false;
38
+
39
+ const startHold = () => {
40
+ if (holdTimeout === -1) {
41
+ if (performOnEnterImmediately()) {
42
+ props.onEnter();
43
+ }
44
+ holdTimeout = setTimeout(() => {
45
+ wasHeld = true;
46
+ props.onHold();
47
+ }, holdThreshold()) as unknown as number;
48
+ }
49
+ return true;
50
+ };
51
+
52
+ const releaseHold = () => {
53
+ if (holdTimeout !== -1) {
54
+ clearTimeout(holdTimeout);
55
+ holdTimeout = -1;
56
+ if (!wasHeld) {
57
+ if (!performOnEnterImmediately()) props.onEnter();
58
+ return;
59
+ }
60
+ props.onRelease?.();
61
+ wasHeld = false;
62
+ }
63
+ return true;
64
+ };
65
+
66
+ return [startHold, releaseHold];
67
+ }
68
+
69
+ export default useHold;
@@ -1,13 +1,29 @@
1
- type ChainableFunction = (...args: unknown[]) => unknown;
1
+ import * as s from 'solid-js';
2
2
 
3
- export function chainFunctions(...args: ChainableFunction[]): ChainableFunction;
4
- export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
3
+ export type AnyFunction = (this: any, ...args: any[]) => any;
5
4
 
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');
5
+ /**
6
+ * take an array of functions and if you return `true` from a function, it will stop the chain
7
+ * @param fns list of functions to chain together, can be `undefined`, `null`, or `false` to skip them
8
+ * @returns a function that will call each function in the list until one returns `true` or all functions are called.
9
+ * If no functions are provided, it will return `undefined`.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function Button (props: NodeProps) {
14
+ * function onEnter (el: ElementNode) {...}
15
+ * return <view onEnter={chainFunctions(props.onEnter, onEnter)} />
16
+ * }
17
+ * ```
18
+ */
19
+ export function chainFunctions<T extends AnyFunction>(...fns: T[]): T;
20
+ export function chainFunctions<T extends AnyFunction>(
21
+ ...fns: (T | undefined | null | false)[]
22
+ ): T | undefined;
23
+ export function chainFunctions(
24
+ ...fns: (AnyFunction | undefined | null | false)[]
25
+ ): AnyFunction | undefined {
26
+ const onlyFunctions = fns.filter((func) => typeof func === 'function');
11
27
  if (onlyFunctions.length === 0) {
12
28
  return undefined;
13
29
  }
@@ -16,7 +32,7 @@ export function chainFunctions<T extends ChainableFunction>(
16
32
  return onlyFunctions[0];
17
33
  }
18
34
 
19
- return function (this: unknown | T, ...innerArgs: unknown[]) {
35
+ return function (...innerArgs) {
20
36
  let result;
21
37
  for (const func of onlyFunctions) {
22
38
  result = func.apply(this, innerArgs);
@@ -27,3 +43,18 @@ export function chainFunctions<T extends ChainableFunction>(
27
43
  return result;
28
44
  };
29
45
  }
46
+
47
+ /**
48
+ * Utility for chaining multiple `ref` assignments with `props.ref` forwarding.
49
+ * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`).
50
+ * @example
51
+ * ```tsx
52
+ * function Button (props: NodeProps) {
53
+ * let localRef: ElementNode | undefined
54
+ * return <view ref={chainRefs(props.ref, el => localRef = el)} />
55
+ * }
56
+ * ```
57
+ */
58
+ export const chainRefs = chainFunctions as <T>(
59
+ ...refs: (s.Ref<T> | undefined)[]
60
+ ) => (el: T) => void;
@@ -1,4 +1,4 @@
1
- import { renderer, type TextureMap } from '@lightningtv/core';
1
+ import { type TextureMap, renderer } from '@lightningtv/core';
2
2
 
3
3
  export interface SpriteDef {
4
4
  name: string | number;
@@ -26,7 +26,7 @@ export function createSpriteMap(
26
26
  y,
27
27
  width,
28
28
  height,
29
- });
29
+ }) as InstanceType<TextureMap['SubTexture']>;
30
30
  return acc;
31
31
  }, {});
32
32
  }
@@ -6,6 +6,15 @@ export function onGridFocus(onSelectedChanged: OnSelectedChanged | undefined) {
6
6
  return function (this: ElementNode) {
7
7
  if (!this || this.children.length === 0) return false;
8
8
 
9
+ // if a child already has focus, assume that should be selected
10
+ this.children.find((child, index) => {
11
+ if (child.states.has(Config.focusStateKey)) {
12
+ this.selected = index;
13
+ return true;
14
+ }
15
+ return false;
16
+ });
17
+
9
18
  this.selected = this.selected || 0;
10
19
  let child = this.selected
11
20
  ? this.children[this.selected]
@@ -73,8 +82,8 @@ export function handleNavigation(
73
82
  return false;
74
83
  }
75
84
  }
76
- const active = this.children[this.selected || 0];
77
- assertTruthy(active instanceof ElementNode);
85
+ const active = this.children[this.selected || 0] || this.children[0];
86
+ if (!(active instanceof ElementNode)) return false;
78
87
  const navigableThis = this as NavigableElement;
79
88
 
80
89
  navigableThis.onSelectedChanged &&
@@ -8,11 +8,18 @@ import type {
8
8
  // Adds properties expected by withScrolling
9
9
  export interface ScrollableElement extends ElementNode {
10
10
  scrollIndex?: number;
11
+ scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center';
11
12
  selected: number;
12
13
  offset?: number;
13
14
  endOffset?: number;
15
+ onScrolled?: (
16
+ elm: ScrollableElement,
17
+ offset: number,
18
+ isInitial: boolean,
19
+ ) => void;
14
20
  _targetPosition?: number;
15
21
  _screenOffset?: number;
22
+ _initialPosition?: number;
16
23
  }
17
24
 
18
25
  // From the renderer, not exported
@@ -48,7 +55,11 @@ export function withScrolling(isRow: boolean) {
48
55
  )
49
56
  return;
50
57
 
51
- const lng = componentRef.lng as INode;
58
+ if (componentRef._initialPosition === undefined) {
59
+ componentRef._initialPosition = componentRef[axis];
60
+ }
61
+
62
+ const lng = componentRef.lng as unknown as INode;
52
63
  const screenSize = isRow ? lng.stage.root.width : lng.stage.root.height;
53
64
  // Determine if movement is incremental or decremental
54
65
  const isIncrementing =
@@ -68,7 +79,14 @@ export function withScrolling(isRow: boolean) {
68
79
 
69
80
  const screenOffset = componentRef._screenOffset;
70
81
  const gap = componentRef.gap || 0;
71
- const scroll = componentRef.scroll || 'auto';
82
+ // when creating we set scroll to always so we setup the right location for selected and scrollIndex
83
+ const scroll =
84
+ componentRef.scroll ||
85
+ (lastSelected === undefined
86
+ ? componentRef.scrollIndex
87
+ ? 'center'
88
+ : 'always'
89
+ : 'auto');
72
90
 
73
91
  // Allows manual position control
74
92
  const targetPosition = componentRef._targetPosition ?? componentRef[axis];
@@ -157,6 +175,11 @@ export function withScrolling(isRow: boolean) {
157
175
 
158
176
  // Update position if it has changed
159
177
  if (componentRef[axis] !== nextPosition) {
178
+ if (componentRef.onScrolled) {
179
+ const isInitial = nextPosition === componentRef._initialPosition;
180
+ componentRef.onScrolled(componentRef, nextPosition, isInitial);
181
+ }
182
+
160
183
  componentRef[axis] = nextPosition;
161
184
  // Store the new position to keep track during animations
162
185
  componentRef._targetPosition = nextPosition;
package/src/render.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  type NodeProps,
5
5
  type TextProps,
6
6
  startLightningRenderer,
7
- type RendererMain,
8
7
  type RendererMainSettings,
9
8
  } from '@lightningtv/core';
10
9
  import nodeOpts from './solidOpts.js';
@@ -14,14 +13,15 @@ import {
14
13
  createRenderEffect,
15
14
  untrack,
16
15
  type JSXElement,
17
- type ValidComponent,
16
+ createRoot,
17
+ type Component,
18
18
  } from 'solid-js';
19
19
  import type { SolidNode } from './types.js';
20
20
  import { activeElement, setActiveElement } from './activeElement.js';
21
21
 
22
22
  const solidRenderer = solidCreateRenderer<SolidNode>(nodeOpts);
23
23
 
24
- let renderer: RendererMain;
24
+ let renderer;
25
25
  export const rootNode = nodeOpts.createElement('App');
26
26
 
27
27
  const render = function (code: () => JSXElement) {
@@ -71,11 +71,13 @@ type Task = () => void;
71
71
  const taskQueue: Task[] = [];
72
72
  let tasksEnabled = false;
73
73
 
74
- createRenderEffect(() => {
75
- // should change whenever a keypress occurs, so we disable the task queue
76
- // until the renderer is idle again.
77
- activeElement();
78
- tasksEnabled = false;
74
+ createRoot(() => {
75
+ createRenderEffect(() => {
76
+ // should change whenever a keypress occurs, so we disable the task queue
77
+ // until the renderer is idle again.
78
+ activeElement();
79
+ tasksEnabled = false;
80
+ });
79
81
  });
80
82
 
81
83
  export function setTasksEnabled(enabled: boolean): void {
@@ -117,10 +119,8 @@ function processTasks(): void {
117
119
  * ```
118
120
  * @description https://www.solidjs.com/docs/latest/api#dynamic
119
121
  */
120
- export function Dynamic<T>(
121
- props: T & {
122
- component?: ValidComponent;
123
- },
122
+ export function Dynamic<T extends Record<string, any>>(
123
+ props: T & { component?: Component<T> | undefined | null },
124
124
  ): JSXElement {
125
125
  const [p, others] = splitProps(props, ['component']);
126
126
 
package/src/solidOpts.ts CHANGED
@@ -9,6 +9,46 @@ import {
9
9
  } from '@lightningtv/core';
10
10
  import type { SolidNode, SolidRendererOptions } from './types.js';
11
11
 
12
+ declare module '@lightningtv/core' {
13
+ interface ElementNode {
14
+ /** @internal for managing series of insertions and deletions */
15
+ _queueDelete?: number;
16
+ preserve?: boolean;
17
+ }
18
+ }
19
+
20
+ Object.defineProperty(ElementNode.prototype, 'preserve', {
21
+ get(): boolean | undefined {
22
+ return this._queueDelete === 0;
23
+ },
24
+ set(v: boolean) {
25
+ this._queueDelete = v ? 0 : undefined;
26
+ },
27
+ });
28
+
29
+ let elementDeleteQueue: ElementNode[] = [];
30
+
31
+ function flushDeleteQueue(): void {
32
+ for (let el of elementDeleteQueue) {
33
+ if (Number(el._queueDelete) < 0) {
34
+ el.destroy();
35
+ }
36
+ el._queueDelete = undefined;
37
+ }
38
+ elementDeleteQueue.length = 0;
39
+ }
40
+
41
+ function pushDeleteQueue(node: ElementNode, n: number): void {
42
+ if (node._queueDelete === undefined) {
43
+ node._queueDelete = n;
44
+ if (elementDeleteQueue.push(node) === 1) {
45
+ queueMicrotask(flushDeleteQueue);
46
+ }
47
+ } else {
48
+ node._queueDelete += n;
49
+ }
50
+ }
51
+
12
52
  export default {
13
53
  createElement(name: string): ElementNode {
14
54
  return new ElementNode(name);
@@ -30,11 +70,14 @@ export default {
30
70
  insertNode(parent: ElementNode, node: SolidNode, anchor: SolidNode): void {
31
71
  log('INSERT: ', parent, node, anchor);
32
72
 
73
+ let prevParent = node.parent;
33
74
  parent.insertChild(node, anchor);
34
- node._queueDelete = false;
35
75
 
36
76
  if (node instanceof ElementNode) {
37
- parent.rendered && node.render(true);
77
+ node.parent!.rendered && node.render(true);
78
+ if (prevParent !== undefined) {
79
+ pushDeleteQueue(node, 1);
80
+ }
38
81
  } else if (isElementText(parent)) {
39
82
  // TextNodes can be placed outside of <text> nodes when <Show> is used as placeholder
40
83
  parent.text = parent.getText();
@@ -45,13 +88,14 @@ export default {
45
88
  },
46
89
  removeNode(parent: ElementNode, node: SolidNode): void {
47
90
  log('REMOVE: ', parent, node);
91
+
48
92
  parent.removeChild(node);
49
- node._queueDelete = true;
93
+
50
94
  if (node instanceof ElementNode) {
51
- // Solid replacesNodes to move them (via insert and remove),
52
- // so we need to wait for the next microtask to destroy the node
53
- // in the event it gets a new parent.
54
- queueMicrotask(() => node.destroy());
95
+ pushDeleteQueue(node, -1);
96
+ } else if (isElementText(parent)) {
97
+ // TextNodes can be placed outside of <text> nodes when <Show> is used as placeholder
98
+ parent.text = parent.getText();
55
99
  }
56
100
  },
57
101
  getParentNode(node: SolidNode): ElementNode | ElementText | undefined {
@@ -1,8 +0,0 @@
1
- import 'solid-js';
2
- declare module 'solid-js' {
3
- namespace JSX {
4
- interface Directives {
5
- model: [() => any, (v: any) => any];
6
- }
7
- }
8
- }