Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@lightningtv/solid

Package Overview
Dependencies
Maintainers
1
Versions
89
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lightningtv/solid - npm Package Compare versions

Comparing version 0.0.3 to 0.0.4

dist/source/primitives/announcer/announcer.js

471

dist/esm/index.js

@@ -1,6 +0,8 @@

import { createSignal, mergeProps as mergeProps$1, createRoot, createRenderEffect, createMemo, createComponent as createComponent$1, untrack, splitProps } from 'solid-js';
import { createSignal, mergeProps as mergeProps$1, createRoot, createRenderEffect, createMemo, createComponent as createComponent$1, untrack, splitProps, createEffect, on, createResource, createComputed, batch } from 'solid-js';
export { ErrorBoundary, For, Index, Match, Show, Suspense, SuspenseList, Switch } from 'solid-js';
import { Config, isInteger, ElementNode, NodeTypes, log, startLightningRenderer } from '@lightningtv/core';
import { Config, isInteger, ElementNode, NodeType, log, startLightningRenderer } from '@lightningtv/core';
export * from '@lightningtv/core';
import { createElement as createElement$1, spread as spread$1 } from '@lightningtv/solid';
import { createElement as createElement$1, spread as spread$1, isArray, activeElement as activeElement$1, isFunc, renderer as renderer$1 } from '@lightningtv/solid';
import { useKeyDownEvent } from '@solid-primitives/keyboard';
import { debounce } from '@solid-primitives/scheduled';

@@ -338,3 +340,3 @@ const [activeElement, setActiveElement] = createSignal(undefined);

return {
type: NodeTypes.Text,
type: NodeType.Text,
text,

@@ -462,3 +464,462 @@ parent: undefined

export { Dynamic, Text, View, activeElement, createComponent, createElement, createTextNode, deg2rad, effect, hexColor, insert, insertNode, memo, mergeProps, render, renderSync, setActiveElement, setProp, spread, startLightning, use };
/**
* Generates a map of event handlers for each key in the KeyMap
*/
const keyMapEntries = {
ArrowLeft: 'Left',
ArrowRight: 'Right',
ArrowUp: 'Up',
ArrowDown: 'Down',
Enter: 'Enter',
l: 'Last',
' ': 'Space',
Backspace: 'Back',
Escape: 'Escape'
};
const [focusPath, setFocusPath] = createSignal([]);
const useFocusManager = userKeyMap => {
const keypressEvent = useKeyDownEvent();
if (userKeyMap) {
// Flatten the userKeyMap to a hash
for (const [key, value] of Object.entries(userKeyMap)) {
if (isArray(value)) {
value.forEach(v => {
keyMapEntries[v] = key;
});
} else {
keyMapEntries[value] = key;
}
}
}
createEffect(on(activeElement$1, (currentFocusedElm, prevFocusedElm, prevFocusPath = []) => {
let current = currentFocusedElm;
const fp = [];
while (current) {
if (!current.states.has('focus')) {
current.states.add('focus');
isFunc(current.onFocus) && current.onFocus.call(current, currentFocusedElm, prevFocusedElm);
}
fp.push(current);
current = current.parent;
}
prevFocusPath.forEach(elm => {
if (!fp.includes(elm)) {
elm.states.remove('focus');
isFunc(elm.onBlur) && elm.onBlur.call(elm, currentFocusedElm, prevFocusedElm);
}
});
setFocusPath(fp);
return fp;
}, {
defer: true
}));
createEffect(() => {
const e = keypressEvent();
if (e) {
// Search keyMap for the value of the pressed key or keyCode if value undefined
const mappedKeyEvent = keyMapEntries[e.key] || keyMapEntries[e.keyCode];
untrack(() => {
const fp = focusPath();
let finalFocusElm = undefined;
for (const elm of fp) {
finalFocusElm = finalFocusElm || elm;
if (mappedKeyEvent) {
const onKeyHandler = elm[`on${mappedKeyEvent}`];
if (isFunc(onKeyHandler)) {
if (onKeyHandler.call(elm, e, elm, finalFocusElm) === true) {
break;
}
}
} else {
console.log(`Unhandled key event: ${e.key || e.keyCode}`);
}
if (isFunc(elm.onKeyPress)) {
if (elm.onKeyPress.call(elm, e, mappedKeyEvent, elm, finalFocusElm) === true) {
break;
}
}
}
return false;
});
}
});
return focusPath;
};
// To use with TS import withPadding and then put withPadding; on the next line to prevent tree shaking
function withPadding(el, padding) {
const pad = padding();
let top, left, right, bottom;
if (Array.isArray(pad)) {
// top right bottom left
if (pad.length === 2) {
top = bottom = pad[0];
left = right = pad[1];
} else if (pad.length === 3) {
top = pad[0];
left = right = pad[1];
bottom = pad[2];
} else {
[top, right, bottom, left] = pad;
}
} else {
top = right = bottom = left = pad;
}
el.onBeforeLayout = (node, size) => {
if (size) {
el.width = el.children.reduce((acc, c) => {
return acc + (c.width || 0);
}, 0) + left + right;
const firstChild = el.children[0];
if (firstChild) {
// set padding or marginLeft for flex
firstChild.x = left;
firstChild.marginLeft = left;
}
let maxHeight = 0;
el.children.forEach(c => {
c.y = top;
c.marginTop = top;
maxHeight = Math.max(maxHeight, c.height || 0);
});
el.height = maxHeight + top + bottom;
// let flex know we need to re-layout
return true;
}
};
}
/* global SpeechSynthesisErrorEvent */
function flattenStrings(series = []) {
const flattenedSeries = [];
let i;
for (i = 0; i < series.length; i++) {
const s = series[i];
if (typeof s === 'string' && !s.includes('PAUSE-')) {
flattenedSeries.push(series[i]);
} else {
break;
}
}
// add a "word boundary" to ensure the Announcer doesn't automatically try to
// interpret strings that look like dates but are not actually dates
// for example, if "Rising Sun" and "1993" are meant to be two separate lines,
// when read together, "Sun 1993" is interpretted as "Sunday 1993"
return [flattenedSeries.join(',\b ')].concat(series.slice(i));
}
function delay(pause) {
return new Promise(resolve => {
setTimeout(resolve, pause);
});
}
/**
* Speak a string
*
* @param phrase Phrase to speak
* @param utterances An array which the new SpeechSynthesisUtterance instance representing this utterance will be appended
* @param lang Language to speak in
* @return {Promise<void>} Promise resolved when the utterance has finished speaking, and rejected if there's an error
*/
function speak(phrase, utterances, lang = 'en-US') {
const synth = window.speechSynthesis;
return new Promise((resolve, reject) => {
const utterance = new SpeechSynthesisUtterance(phrase);
utterance.lang = lang;
utterance.onend = () => {
resolve();
};
utterance.onerror = e => {
reject(e);
};
utterances.push(utterance);
synth.speak(utterance);
});
}
function speakSeries(series, lang, root = true) {
const synth = window.speechSynthesis;
const remainingPhrases = flattenStrings(Array.isArray(series) ? series : [series]);
const nestedSeriesResults = [];
/*
We hold this array of SpeechSynthesisUtterances in order to prevent them from being
garbage collected prematurely on STB hardware which can cause the 'onend' events of
utterances to not fire consistently.
*/
const utterances = [];
let active = true;
const seriesChain = (async () => {
try {
while (active && remainingPhrases.length) {
const phrase = await Promise.resolve(remainingPhrases.shift());
if (!active) {
// Exit
// Need to check this after the await in case it was cancelled in between
break;
} else if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
// Pause it
let pause = Number(phrase.split('PAUSE-')[1]) * 1000;
if (isNaN(pause)) {
pause = 0;
}
await delay(pause);
} else if (typeof phrase === 'string' && phrase.length) {
// Speak it
const totalRetries = 3;
let retriesLeft = totalRetries;
while (active && retriesLeft > 0) {
try {
await speak(phrase, utterances, lang);
retriesLeft = 0;
} catch (e) {
// eslint-disable-next-line no-undef
if (e instanceof SpeechSynthesisErrorEvent) {
if (e.error === 'network') {
retriesLeft--;
console.warn(`Speech synthesis network error. Retries left: ${retriesLeft}`);
await delay(500 * (totalRetries - retriesLeft));
} else if (e.error === 'canceled' || e.error === 'interrupted') {
// Cancel or interrupt error (ignore)
retriesLeft = 0;
} else {
throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
}
} else {
throw e;
}
}
}
} else if (typeof phrase === 'function') {
const seriesResult = speakSeries(phrase(), lang, false);
nestedSeriesResults.push(seriesResult);
await seriesResult.series;
} else if (Array.isArray(phrase)) {
// Speak it (recursively)
const seriesResult = speakSeries(phrase, lang, false);
nestedSeriesResults.push(seriesResult);
await seriesResult.series;
}
}
} finally {
active = false;
}
})();
return {
series: seriesChain,
get active() {
return active;
},
append: toSpeak => {
remainingPhrases.push(toSpeak);
},
cancel: () => {
if (!active) {
return;
}
if (root) {
synth.cancel();
}
nestedSeriesResults.forEach(nestedSeriesResults => {
nestedSeriesResults.cancel();
});
active = false;
}
};
}
let currentSeries;
function SpeechEngine (toSpeak, lang = 'en-US') {
currentSeries && currentSeries.cancel();
currentSeries = speakSeries(toSpeak, lang);
return currentSeries;
}
let resetFocusPathTimer;
let prevFocusPath = [];
let currentlySpeaking;
let voiceOutDisabled = false;
const fiveMinutes = 300000;
function debounceWithFlush(callback, time) {
const trigger = debounce(callback, time);
let scopedValue;
const debounced = newValue => {
scopedValue = newValue;
trigger(newValue);
};
debounced.flush = () => {
trigger.clear();
callback(scopedValue);
};
debounced.clear = trigger.clear;
return debounced;
}
function getElmName(elm) {
return elm.id || elm.name;
}
function onFocusChangeCore(focusPath = []) {
if (!Announcer.onFocusChange || !Announcer.enabled) {
return;
}
const loaded = focusPath.every(elm => !elm.loading);
const focusDiff = focusPath.filter(elm => !prevFocusPath.includes(elm));
resetFocusPathTimer();
if (!loaded && Announcer.onFocusChange) {
Announcer.onFocusChange([]);
return;
}
prevFocusPath = focusPath.slice(0);
const toAnnounceText = [];
const toAnnounce = focusDiff.reduce((acc, elm) => {
if (elm.announce) {
acc.push([getElmName(elm), 'Announce', elm.announce]);
toAnnounceText.push(elm.announce);
} else if (elm.title) {
acc.push([getElmName(elm), 'Title', elm.title]);
toAnnounceText.push(elm.title);
} else {
acc.push([getElmName(elm), 'No Announce', '']);
}
return acc;
}, []);
focusDiff.reverse().reduce((acc, elm) => {
if (elm.announceContext) {
acc.push([getElmName(elm), 'Context', elm.announceContext]);
toAnnounceText.push(elm.announceContext);
} else {
acc.push([getElmName(elm), 'No Context', '']);
}
return acc;
}, toAnnounce);
if (Announcer.debug) {
console.table(toAnnounce);
}
if (toAnnounceText.length) {
return Announcer.speak(toAnnounceText.reduce((acc, val) => acc.concat(val), []));
}
}
function textToSpeech(toSpeak) {
if (voiceOutDisabled) {
return;
}
return currentlySpeaking = SpeechEngine(toSpeak);
}
const Announcer = {
debug: false,
enabled: true,
cancel: function () {
currentlySpeaking && currentlySpeaking.cancel();
},
clearPrevFocus: function (depth = 0) {
prevFocusPath = prevFocusPath.slice(0, depth);
resetFocusPathTimer();
},
speak: function (text, {
append = false,
notification = false
} = {}) {
if (Announcer.onFocusChange && Announcer.enabled) {
Announcer.onFocusChange.flush();
if (append && currentlySpeaking && currentlySpeaking.active) {
currentlySpeaking.append(text);
} else {
Announcer.cancel();
textToSpeech(text);
}
if (notification) {
voiceOutDisabled = true;
currentlySpeaking?.series.finally(() => {
voiceOutDisabled = false;
Announcer.refresh();
}).catch(console.error);
}
}
return currentlySpeaking;
},
refresh: function (depth = 0) {
Announcer.clearPrevFocus(depth);
Announcer.onFocusChange && Announcer.onFocusChange(untrack(() => focusPath()));
},
setupTimers: function ({
focusDebounce = 400,
focusChangeTimeout = fiveMinutes
} = {}) {
Announcer.onFocusChange = debounceWithFlush(onFocusChangeCore, focusDebounce);
resetFocusPathTimer = debounceWithFlush(() => {
// Reset focus path for full announce
prevFocusPath = [];
}, focusChangeTimeout);
}
};
const useAnnouncer = () => {
Announcer.setupTimers();
createEffect(on(focusPath, Announcer.onFocusChange, {
defer: true
}));
return Announcer;
};
// Adopted from https://github.com/solidjs-community/solid-primitives/blob/main/packages/pagination/src/index.ts
// As we don't have intersection observer in Lightning, we can't use the original implementation
/**
* Provides an easy way to implement infinite items.
*
* ```ts
* const [items, loader, { item, setItem, setItems, end, setEnd }] = createInfiniteScroll(fetcher);
* ```
* @param fetcher `(item: number) => Promise<T[]>`
* @return `items()` is an accessor contains array of contents
* @property `items.loading` is a boolean indicator for the loading state
* @property `items.error` contains any error encountered
* @method `page` is an accessor that contains page number
* @method `setPage` allows to manually change the page number
* @method `setItems` allows to manually change the contents of the item
* @method `end` is a boolean indicator for end of the item
* @method `setEnd` allows to manually change the end
*/
function createInfiniteItems(fetcher) {
const [items, setItems] = createSignal([]);
const [page, setPage] = createSignal(0);
const [end, setEnd] = createSignal(false);
const [contents] = createResource(page, fetcher);
createComputed(() => {
const content = contents();
if (!content) return;
batch(() => {
if (content.length === 0) setEnd(true);
setItems(p => [...p, ...content]);
});
});
return [items, {
page,
setPage,
setItems,
end,
setEnd
}];
}
function createSpriteMap(src, subTextures) {
const spriteMapTexture = renderer$1.createTexture('ImageTexture', {
src
});
return subTextures.reduce((acc, t) => {
const {
x,
y,
width,
height
} = t;
acc[t.name] = renderer$1.createTexture('SubTexture', {
texture: spriteMapTexture,
x,
y,
width,
height
});
return acc;
}, {});
}
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 };
//# sourceMappingURL=index.js.map

@@ -9,1 +9,2 @@ import './jsx-runtime.js';

export * from './render.js';
export * from './primitives/index.js';

4

dist/source/solidOpts.js
import { assertTruthy } from '@lightningjs/renderer/utils';
import { ElementNode, log } from '@lightningtv/core';
import { ElementNode, NodeType, log } from '@lightningtv/core';
export default {

@@ -9,3 +9,3 @@ createElement(name) {

// A text node is just a string - not the <text> node
return { type: 2 /* NodeTypes.Text */, text, parent: undefined };
return { type: NodeType.Text, text, parent: undefined };
},

@@ -12,0 +12,0 @@ replaceText(node, value) {

@@ -9,1 +9,2 @@ import './jsx-runtime.js';

export * from './render.js';
export * from './primitives/index.js';
{
"name": "@lightningtv/solid",
"version": "0.0.3",
"version": "0.0.4",
"description": "Lightning Renderer for Solid Universal",

@@ -36,3 +36,6 @@ "type": "module",

"dependencies": {
"@lightningtv/core": "^0.0.3"
"@lightningtv/core": "^0.0.5",
"@lightningtv/solid": "file:",
"@solid-primitives/keyboard": "^1.2.8",
"@solid-primitives/scheduled": "^1.4.3"
},

@@ -39,0 +42,0 @@ "devDependencies": {

@@ -18,1 +18,2 @@ import './jsx-runtime.js';

export * from './render.js';
export * from './primitives/index.js';
import { assertTruthy } from '@lightningjs/renderer/utils';
import type { SolidNode, TextNode } from '@lightningtv/core';
import { ElementNode, NodeTypes, log } from '@lightningtv/core';
import { ElementNode, NodeType, log } from '@lightningtv/core';
import type { createRenderer } from 'solid-js/universal';

@@ -16,3 +16,3 @@

// A text node is just a string - not the <text> node
return { type: NodeTypes.Text, text, parent: undefined };
return { type: NodeType.Text, text, parent: undefined };
},

@@ -19,0 +19,0 @@ replaceText(node: TextNode, value: string): void {

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc