@lightningtv/solid 0.0.3 → 0.0.5

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 (33) hide show
  1. package/dist/esm/index.js +466 -5
  2. package/dist/esm/index.js.map +1 -1
  3. package/dist/source/index.js +1 -0
  4. package/dist/source/primitives/announcer/announcer.js +121 -0
  5. package/dist/source/primitives/announcer/index.js +8 -0
  6. package/dist/source/primitives/announcer/speech.js +152 -0
  7. package/dist/source/primitives/createInfiniteItems.js +45 -0
  8. package/dist/source/primitives/createSpriteMap.js +17 -0
  9. package/dist/source/primitives/index.js +5 -0
  10. package/dist/source/primitives/useFocusManager.js +88 -0
  11. package/dist/source/primitives/withPadding.js +48 -0
  12. package/dist/source/solidOpts.js +2 -2
  13. package/dist/types/index.d.ts +1 -0
  14. package/dist/types/primitives/announcer/announcer.d.ts +37 -0
  15. package/dist/types/primitives/announcer/index.d.ts +2 -0
  16. package/dist/types/primitives/announcer/speech.d.ts +10 -0
  17. package/dist/types/primitives/createInfiniteItems.d.ts +27 -0
  18. package/dist/types/primitives/createSpriteMap.d.ts +8 -0
  19. package/dist/types/primitives/index.d.ts +5 -0
  20. package/dist/types/primitives/useFocusManager.d.ts +47 -0
  21. package/dist/types/primitives/withPadding.d.ts +3 -0
  22. package/package.json +9 -2
  23. package/src/index.ts +1 -0
  24. package/src/primitives/announcer/announcer.ts +190 -0
  25. package/src/primitives/announcer/index.ts +10 -0
  26. package/src/primitives/announcer/speech.ts +174 -0
  27. package/src/primitives/createInfiniteItems.ts +66 -0
  28. package/src/primitives/createSpriteMap.ts +31 -0
  29. package/src/primitives/index.ts +5 -0
  30. package/src/primitives/jsx-runtime.d.ts +11 -0
  31. package/src/primitives/useFocusManager.ts +200 -0
  32. package/src/primitives/withPadding.ts +56 -0
  33. package/src/solidOpts.ts +2 -2
package/dist/esm/index.js CHANGED
@@ -1,8 +1,10 @@
1
- import { createSignal, mergeProps as mergeProps$1, createRoot, createRenderEffect, createMemo, createComponent as createComponent$1, untrack, splitProps } from 'solid-js';
1
+ import { createSignal, mergeProps as mergeProps$1, createRoot, createRenderEffect, createMemo, createComponent as createComponent$1, untrack, splitProps, createEffect, on, createResource, createComputed, batch } from 'solid-js';
2
2
  export { ErrorBoundary, For, Index, Match, Show, Suspense, SuspenseList, Switch } from 'solid-js';
3
- import { Config, isInteger, ElementNode, NodeTypes, log, startLightningRenderer } from '@lightningtv/core';
3
+ import { Config, isInteger, ElementNode, NodeType, log, startLightningRenderer } from '@lightningtv/core';
4
4
  export * from '@lightningtv/core';
5
- import { createElement as createElement$1, spread as spread$1 } from '@lightningtv/solid';
5
+ import { createElement as createElement$1, spread as spread$1, isArray, activeElement as activeElement$1, isFunc, renderer as renderer$1 } from '@lightningtv/solid';
6
+ import { useKeyDownEvent } from '@solid-primitives/keyboard';
7
+ import { debounce } from '@solid-primitives/scheduled';
6
8
 
7
9
  const [activeElement, setActiveElement] = createSignal(undefined);
8
10
  Config.setActiveElement = setActiveElement;
@@ -335,7 +337,7 @@ var nodeOpts = {
335
337
  createTextNode(text) {
336
338
  // A text node is just a string - not the <text> node
337
339
  return {
338
- type: NodeTypes.Text,
340
+ type: NodeType.Text,
339
341
  text,
340
342
  parent: undefined
341
343
  };
@@ -458,5 +460,464 @@ function Dynamic(props) {
458
460
  });
459
461
  }
460
462
 
461
- export { Dynamic, Text, View, activeElement, createComponent, createElement, createTextNode, deg2rad, effect, hexColor, insert, insertNode, memo, mergeProps, render, renderSync, setActiveElement, setProp, spread, startLightning, use };
463
+ /**
464
+ * Generates a map of event handlers for each key in the KeyMap
465
+ */
466
+
467
+ const keyMapEntries = {
468
+ ArrowLeft: 'Left',
469
+ ArrowRight: 'Right',
470
+ ArrowUp: 'Up',
471
+ ArrowDown: 'Down',
472
+ Enter: 'Enter',
473
+ l: 'Last',
474
+ ' ': 'Space',
475
+ Backspace: 'Back',
476
+ Escape: 'Escape'
477
+ };
478
+ const [focusPath, setFocusPath] = createSignal([]);
479
+ const useFocusManager = userKeyMap => {
480
+ const keypressEvent = useKeyDownEvent();
481
+ if (userKeyMap) {
482
+ // Flatten the userKeyMap to a hash
483
+ for (const [key, value] of Object.entries(userKeyMap)) {
484
+ if (isArray(value)) {
485
+ value.forEach(v => {
486
+ keyMapEntries[v] = key;
487
+ });
488
+ } else {
489
+ keyMapEntries[value] = key;
490
+ }
491
+ }
492
+ }
493
+ createEffect(on(activeElement$1, (currentFocusedElm, prevFocusedElm, prevFocusPath = []) => {
494
+ let current = currentFocusedElm;
495
+ const fp = [];
496
+ while (current) {
497
+ if (!current.states.has('focus')) {
498
+ current.states.add('focus');
499
+ isFunc(current.onFocus) && current.onFocus.call(current, currentFocusedElm, prevFocusedElm);
500
+ }
501
+ fp.push(current);
502
+ current = current.parent;
503
+ }
504
+ prevFocusPath.forEach(elm => {
505
+ if (!fp.includes(elm)) {
506
+ elm.states.remove('focus');
507
+ isFunc(elm.onBlur) && elm.onBlur.call(elm, currentFocusedElm, prevFocusedElm);
508
+ }
509
+ });
510
+ setFocusPath(fp);
511
+ return fp;
512
+ }, {
513
+ defer: true
514
+ }));
515
+ createEffect(() => {
516
+ const e = keypressEvent();
517
+ if (e) {
518
+ // Search keyMap for the value of the pressed key or keyCode if value undefined
519
+ const mappedKeyEvent = keyMapEntries[e.key] || keyMapEntries[e.keyCode];
520
+ untrack(() => {
521
+ const fp = focusPath();
522
+ let finalFocusElm = undefined;
523
+ for (const elm of fp) {
524
+ finalFocusElm = finalFocusElm || elm;
525
+ if (mappedKeyEvent) {
526
+ const onKeyHandler = elm[`on${mappedKeyEvent}`];
527
+ if (isFunc(onKeyHandler)) {
528
+ if (onKeyHandler.call(elm, e, elm, finalFocusElm) === true) {
529
+ break;
530
+ }
531
+ }
532
+ } else {
533
+ console.log(`Unhandled key event: ${e.key || e.keyCode}`);
534
+ }
535
+ if (isFunc(elm.onKeyPress)) {
536
+ if (elm.onKeyPress.call(elm, e, mappedKeyEvent, elm, finalFocusElm) === true) {
537
+ break;
538
+ }
539
+ }
540
+ }
541
+ return false;
542
+ });
543
+ }
544
+ });
545
+ return focusPath;
546
+ };
547
+
548
+ // To use with TS import withPadding and then put withPadding; on the next line to prevent tree shaking
549
+ function withPadding(el, padding) {
550
+ const pad = padding();
551
+ let top, left, right, bottom;
552
+ if (Array.isArray(pad)) {
553
+ // top right bottom left
554
+ if (pad.length === 2) {
555
+ top = bottom = pad[0];
556
+ left = right = pad[1];
557
+ } else if (pad.length === 3) {
558
+ top = pad[0];
559
+ left = right = pad[1];
560
+ bottom = pad[2];
561
+ } else {
562
+ [top, right, bottom, left] = pad;
563
+ }
564
+ } else {
565
+ top = right = bottom = left = pad;
566
+ }
567
+ el.onBeforeLayout = (node, size) => {
568
+ if (size) {
569
+ el.width = el.children.reduce((acc, c) => {
570
+ return acc + (c.width || 0);
571
+ }, 0) + left + right;
572
+ const firstChild = el.children[0];
573
+ if (firstChild) {
574
+ // set padding or marginLeft for flex
575
+ firstChild.x = left;
576
+ firstChild.marginLeft = left;
577
+ }
578
+ let maxHeight = 0;
579
+ el.children.forEach(c => {
580
+ c.y = top;
581
+ c.marginTop = top;
582
+ maxHeight = Math.max(maxHeight, c.height || 0);
583
+ });
584
+ el.height = maxHeight + top + bottom;
585
+ // let flex know we need to re-layout
586
+ return true;
587
+ }
588
+ };
589
+ }
590
+
591
+ /* global SpeechSynthesisErrorEvent */
592
+ function flattenStrings(series = []) {
593
+ const flattenedSeries = [];
594
+ let i;
595
+ for (i = 0; i < series.length; i++) {
596
+ const s = series[i];
597
+ if (typeof s === 'string' && !s.includes('PAUSE-')) {
598
+ flattenedSeries.push(series[i]);
599
+ } else {
600
+ break;
601
+ }
602
+ }
603
+ // add a "word boundary" to ensure the Announcer doesn't automatically try to
604
+ // interpret strings that look like dates but are not actually dates
605
+ // for example, if "Rising Sun" and "1993" are meant to be two separate lines,
606
+ // when read together, "Sun 1993" is interpretted as "Sunday 1993"
607
+ return [flattenedSeries.join(',\b ')].concat(series.slice(i));
608
+ }
609
+ function delay(pause) {
610
+ return new Promise(resolve => {
611
+ setTimeout(resolve, pause);
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Speak a string
617
+ *
618
+ * @param phrase Phrase to speak
619
+ * @param utterances An array which the new SpeechSynthesisUtterance instance representing this utterance will be appended
620
+ * @param lang Language to speak in
621
+ * @return {Promise<void>} Promise resolved when the utterance has finished speaking, and rejected if there's an error
622
+ */
623
+ function speak(phrase, utterances, lang = 'en-US') {
624
+ const synth = window.speechSynthesis;
625
+ return new Promise((resolve, reject) => {
626
+ const utterance = new SpeechSynthesisUtterance(phrase);
627
+ utterance.lang = lang;
628
+ utterance.onend = () => {
629
+ resolve();
630
+ };
631
+ utterance.onerror = e => {
632
+ reject(e);
633
+ };
634
+ utterances.push(utterance);
635
+ synth.speak(utterance);
636
+ });
637
+ }
638
+ function speakSeries(series, lang, root = true) {
639
+ const synth = window.speechSynthesis;
640
+ const remainingPhrases = flattenStrings(Array.isArray(series) ? series : [series]);
641
+ const nestedSeriesResults = [];
642
+ /*
643
+ We hold this array of SpeechSynthesisUtterances in order to prevent them from being
644
+ garbage collected prematurely on STB hardware which can cause the 'onend' events of
645
+ utterances to not fire consistently.
646
+ */
647
+ const utterances = [];
648
+ let active = true;
649
+ const seriesChain = (async () => {
650
+ try {
651
+ while (active && remainingPhrases.length) {
652
+ const phrase = await Promise.resolve(remainingPhrases.shift());
653
+ if (!active) {
654
+ // Exit
655
+ // Need to check this after the await in case it was cancelled in between
656
+ break;
657
+ } else if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
658
+ // Pause it
659
+ let pause = Number(phrase.split('PAUSE-')[1]) * 1000;
660
+ if (isNaN(pause)) {
661
+ pause = 0;
662
+ }
663
+ await delay(pause);
664
+ } else if (typeof phrase === 'string' && phrase.length) {
665
+ // Speak it
666
+ const totalRetries = 3;
667
+ let retriesLeft = totalRetries;
668
+ while (active && retriesLeft > 0) {
669
+ try {
670
+ await speak(phrase, utterances, lang);
671
+ retriesLeft = 0;
672
+ } catch (e) {
673
+ // eslint-disable-next-line no-undef
674
+ if (e instanceof SpeechSynthesisErrorEvent) {
675
+ if (e.error === 'network') {
676
+ retriesLeft--;
677
+ console.warn(`Speech synthesis network error. Retries left: ${retriesLeft}`);
678
+ await delay(500 * (totalRetries - retriesLeft));
679
+ } else if (e.error === 'canceled' || e.error === 'interrupted') {
680
+ // Cancel or interrupt error (ignore)
681
+ retriesLeft = 0;
682
+ } else {
683
+ throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
684
+ }
685
+ } else {
686
+ throw e;
687
+ }
688
+ }
689
+ }
690
+ } else if (typeof phrase === 'function') {
691
+ const seriesResult = speakSeries(phrase(), lang, false);
692
+ nestedSeriesResults.push(seriesResult);
693
+ await seriesResult.series;
694
+ } else if (Array.isArray(phrase)) {
695
+ // Speak it (recursively)
696
+ const seriesResult = speakSeries(phrase, lang, false);
697
+ nestedSeriesResults.push(seriesResult);
698
+ await seriesResult.series;
699
+ }
700
+ }
701
+ } finally {
702
+ active = false;
703
+ }
704
+ })();
705
+ return {
706
+ series: seriesChain,
707
+ get active() {
708
+ return active;
709
+ },
710
+ append: toSpeak => {
711
+ remainingPhrases.push(toSpeak);
712
+ },
713
+ cancel: () => {
714
+ if (!active) {
715
+ return;
716
+ }
717
+ if (root) {
718
+ synth.cancel();
719
+ }
720
+ nestedSeriesResults.forEach(nestedSeriesResults => {
721
+ nestedSeriesResults.cancel();
722
+ });
723
+ active = false;
724
+ }
725
+ };
726
+ }
727
+ let currentSeries;
728
+ function SpeechEngine (toSpeak, lang = 'en-US') {
729
+ currentSeries && currentSeries.cancel();
730
+ currentSeries = speakSeries(toSpeak, lang);
731
+ return currentSeries;
732
+ }
733
+
734
+ let resetFocusPathTimer;
735
+ let prevFocusPath = [];
736
+ let currentlySpeaking;
737
+ let voiceOutDisabled = false;
738
+ const fiveMinutes = 300000;
739
+ function debounceWithFlush(callback, time) {
740
+ const trigger = debounce(callback, time);
741
+ let scopedValue;
742
+ const debounced = newValue => {
743
+ scopedValue = newValue;
744
+ trigger(newValue);
745
+ };
746
+ debounced.flush = () => {
747
+ trigger.clear();
748
+ callback(scopedValue);
749
+ };
750
+ debounced.clear = trigger.clear;
751
+ return debounced;
752
+ }
753
+ function getElmName(elm) {
754
+ return elm.id || elm.name;
755
+ }
756
+ function onFocusChangeCore(focusPath = []) {
757
+ if (!Announcer.onFocusChange || !Announcer.enabled) {
758
+ return;
759
+ }
760
+ const loaded = focusPath.every(elm => !elm.loading);
761
+ const focusDiff = focusPath.filter(elm => !prevFocusPath.includes(elm));
762
+ resetFocusPathTimer();
763
+ if (!loaded && Announcer.onFocusChange) {
764
+ Announcer.onFocusChange([]);
765
+ return;
766
+ }
767
+ prevFocusPath = focusPath.slice(0);
768
+ const toAnnounceText = [];
769
+ const toAnnounce = focusDiff.reduce((acc, elm) => {
770
+ if (elm.announce) {
771
+ acc.push([getElmName(elm), 'Announce', elm.announce]);
772
+ toAnnounceText.push(elm.announce);
773
+ } else if (elm.title) {
774
+ acc.push([getElmName(elm), 'Title', elm.title]);
775
+ toAnnounceText.push(elm.title);
776
+ } else {
777
+ acc.push([getElmName(elm), 'No Announce', '']);
778
+ }
779
+ return acc;
780
+ }, []);
781
+ focusDiff.reverse().reduce((acc, elm) => {
782
+ if (elm.announceContext) {
783
+ acc.push([getElmName(elm), 'Context', elm.announceContext]);
784
+ toAnnounceText.push(elm.announceContext);
785
+ } else {
786
+ acc.push([getElmName(elm), 'No Context', '']);
787
+ }
788
+ return acc;
789
+ }, toAnnounce);
790
+ if (Announcer.debug) {
791
+ console.table(toAnnounce);
792
+ }
793
+ if (toAnnounceText.length) {
794
+ return Announcer.speak(toAnnounceText.reduce((acc, val) => acc.concat(val), []));
795
+ }
796
+ }
797
+ function textToSpeech(toSpeak) {
798
+ if (voiceOutDisabled) {
799
+ return;
800
+ }
801
+ return currentlySpeaking = SpeechEngine(toSpeak);
802
+ }
803
+ const Announcer = {
804
+ debug: false,
805
+ enabled: true,
806
+ cancel: function () {
807
+ currentlySpeaking && currentlySpeaking.cancel();
808
+ },
809
+ clearPrevFocus: function (depth = 0) {
810
+ prevFocusPath = prevFocusPath.slice(0, depth);
811
+ resetFocusPathTimer();
812
+ },
813
+ speak: function (text, {
814
+ append = false,
815
+ notification = false
816
+ } = {}) {
817
+ if (Announcer.onFocusChange && Announcer.enabled) {
818
+ Announcer.onFocusChange.flush();
819
+ if (append && currentlySpeaking && currentlySpeaking.active) {
820
+ currentlySpeaking.append(text);
821
+ } else {
822
+ Announcer.cancel();
823
+ textToSpeech(text);
824
+ }
825
+ if (notification) {
826
+ voiceOutDisabled = true;
827
+ currentlySpeaking?.series.finally(() => {
828
+ voiceOutDisabled = false;
829
+ Announcer.refresh();
830
+ }).catch(console.error);
831
+ }
832
+ }
833
+ return currentlySpeaking;
834
+ },
835
+ refresh: function (depth = 0) {
836
+ Announcer.clearPrevFocus(depth);
837
+ Announcer.onFocusChange && Announcer.onFocusChange(untrack(() => focusPath()));
838
+ },
839
+ setupTimers: function ({
840
+ focusDebounce = 400,
841
+ focusChangeTimeout = fiveMinutes
842
+ } = {}) {
843
+ Announcer.onFocusChange = debounceWithFlush(onFocusChangeCore, focusDebounce);
844
+ resetFocusPathTimer = debounceWithFlush(() => {
845
+ // Reset focus path for full announce
846
+ prevFocusPath = [];
847
+ }, focusChangeTimeout);
848
+ }
849
+ };
850
+
851
+ const useAnnouncer = () => {
852
+ Announcer.setupTimers();
853
+ createEffect(on(focusPath, Announcer.onFocusChange, {
854
+ defer: true
855
+ }));
856
+ return Announcer;
857
+ };
858
+
859
+ // Adopted from https://github.com/solidjs-community/solid-primitives/blob/main/packages/pagination/src/index.ts
860
+ // As we don't have intersection observer in Lightning, we can't use the original implementation
861
+
862
+ /**
863
+ * Provides an easy way to implement infinite items.
864
+ *
865
+ * ```ts
866
+ * const [items, loader, { item, setItem, setItems, end, setEnd }] = createInfiniteScroll(fetcher);
867
+ * ```
868
+ * @param fetcher `(item: number) => Promise<T[]>`
869
+ * @return `items()` is an accessor contains array of contents
870
+ * @property `items.loading` is a boolean indicator for the loading state
871
+ * @property `items.error` contains any error encountered
872
+ * @method `page` is an accessor that contains page number
873
+ * @method `setPage` allows to manually change the page number
874
+ * @method `setItems` allows to manually change the contents of the item
875
+ * @method `end` is a boolean indicator for end of the item
876
+ * @method `setEnd` allows to manually change the end
877
+ */
878
+ function createInfiniteItems(fetcher) {
879
+ const [items, setItems] = createSignal([]);
880
+ const [page, setPage] = createSignal(0);
881
+ const [end, setEnd] = createSignal(false);
882
+ const [contents] = createResource(page, fetcher);
883
+ createComputed(() => {
884
+ const content = contents();
885
+ if (!content) return;
886
+ batch(() => {
887
+ if (content.length === 0) setEnd(true);
888
+ setItems(p => [...p, ...content]);
889
+ });
890
+ });
891
+ return [items, {
892
+ page,
893
+ setPage,
894
+ setItems,
895
+ end,
896
+ setEnd
897
+ }];
898
+ }
899
+
900
+ function createSpriteMap(src, subTextures) {
901
+ const spriteMapTexture = renderer$1.createTexture('ImageTexture', {
902
+ src
903
+ });
904
+ return subTextures.reduce((acc, t) => {
905
+ const {
906
+ x,
907
+ y,
908
+ width,
909
+ height
910
+ } = t;
911
+ acc[t.name] = renderer$1.createTexture('SubTexture', {
912
+ texture: spriteMapTexture,
913
+ x,
914
+ y,
915
+ width,
916
+ height
917
+ });
918
+ return acc;
919
+ }, {});
920
+ }
921
+
922
+ export { Dynamic, Text, View, activeElement, createComponent, createElement, createInfiniteItems, createSpriteMap, createTextNode, deg2rad, effect, focusPath, hexColor, insert, insertNode, memo, mergeProps, render, renderSync, setActiveElement, setProp, spread, startLightning, use, useAnnouncer, useFocusManager, withPadding };
462
923
  //# sourceMappingURL=index.js.map