@lightningjs/blits 1.33.0 → 1.34.0
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/CHANGELOG.md +11 -0
- package/NOTICE +4 -0
- package/index.d.ts +4 -2
- package/package.json +4 -2
- package/src/announcer/announcer.js +1 -0
- package/src/announcer/speechSynthesis.js +21 -4
- package/src/application.js +5 -1
- package/src/component/setup/state.js +8 -2
- package/src/plugins/index.js +1 -0
- package/src/plugins/storage/localCookie.js +133 -0
- package/src/plugins/storage/storage.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
_10 Jul 2025_
|
|
4
|
+
|
|
5
|
+
## v1.34.0
|
|
6
|
+
|
|
7
|
+
- Added storage plugin
|
|
8
|
+
- Improved test coverage
|
|
9
|
+
- Fixed announcer plugin to work on RDK devices where early GC was happening
|
|
10
|
+
- Added check to throttle only the same input key in a row
|
|
11
|
+
- Updated docs on transitions
|
|
12
|
+
- Added warning when using `hasFocus` in component state
|
|
13
|
+
|
|
3
14
|
_08 Jul 2025_
|
|
4
15
|
|
|
5
16
|
## v1.33.0
|
package/NOTICE
CHANGED
|
@@ -10,3 +10,7 @@ within this component.
|
|
|
10
10
|
This repository uses components from the Lightning-SDK. Any components from the Lightning-SDK are:
|
|
11
11
|
Copyright (c) 2020 Metrological
|
|
12
12
|
Licensed under the Apache License, Version 2.0
|
|
13
|
+
|
|
14
|
+
This repository uses components from WebPlatformForEmbedded
|
|
15
|
+
Copyright (c) 2019 Metrological
|
|
16
|
+
Licensed under the Apache License, Version 2.0
|
package/index.d.ts
CHANGED
|
@@ -970,8 +970,10 @@ declare module '@lightningjs/blits' {
|
|
|
970
970
|
/**
|
|
971
971
|
* Input throttle time in milliseconds to prevent rapid successive inputs
|
|
972
972
|
*
|
|
973
|
-
* Within the throttle window, only one input will be processed immediately
|
|
974
|
-
* inputs are ignored until the scheduled input is processed.
|
|
973
|
+
* Within the throttle window, only one input will be processed immediately.
|
|
974
|
+
* Subsequent inputs _of the same key_ are ignored until the scheduled input is processed.
|
|
975
|
+
*
|
|
976
|
+
* Pressing a different key will be processed immediately.
|
|
975
977
|
*
|
|
976
978
|
* Set to `0` to disable input throttling.
|
|
977
979
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "Blits: The Lightning 3 App Development Framework",
|
|
5
5
|
"bin": "bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "c8
|
|
16
|
+
"test": "c8 node scripts/runTests.js",
|
|
17
|
+
"test:ci": "c8 npm run test:run",
|
|
17
18
|
"test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff",
|
|
18
19
|
"lint": "eslint '**/*.js'",
|
|
19
20
|
"lint:fix": "eslint '**/*.js' --fix",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"eslint": "^8.8.0",
|
|
39
40
|
"eslint-config-prettier": "^8.3.0",
|
|
40
41
|
"eslint-plugin-prettier": "^4.0.0",
|
|
42
|
+
"fast-glob": "^3.3.3",
|
|
41
43
|
"global-jsdom": "24.0.0",
|
|
42
44
|
"husky": "^9.1.7",
|
|
43
45
|
"jsdom": "24.0.0",
|
|
@@ -21,9 +21,17 @@ const syn = window.speechSynthesis
|
|
|
21
21
|
|
|
22
22
|
const isAndroid = /android/i.test((window.navigator || {}).userAgent || '')
|
|
23
23
|
|
|
24
|
+
const utterances = new Map() // Strong references with unique keys
|
|
25
|
+
|
|
24
26
|
let initialized = false
|
|
25
27
|
let infinityTimer = null
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
const clear = () => {
|
|
30
|
+
if (infinityTimer) {
|
|
31
|
+
clearTimeout(infinityTimer)
|
|
32
|
+
infinityTimer = null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
const resumeInfinity = (target) => {
|
|
29
37
|
if (!target || infinityTimer) {
|
|
@@ -31,7 +39,9 @@ const resumeInfinity = (target) => {
|
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
syn.pause()
|
|
34
|
-
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
syn.resume()
|
|
44
|
+
}, 0)
|
|
35
45
|
|
|
36
46
|
infinityTimer = setTimeout(() => {
|
|
37
47
|
resumeInfinity(target)
|
|
@@ -47,18 +57,20 @@ const defaultUtteranceProps = {
|
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
const initialize = () => {
|
|
50
|
-
|
|
60
|
+
const voices = syn.getVoices()
|
|
61
|
+
defaultUtteranceProps.voice = voices[0] || null
|
|
51
62
|
initialized = true
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
const speak = (options) => {
|
|
55
66
|
const utterance = new SpeechSynthesisUtterance(options.message)
|
|
56
|
-
|
|
67
|
+
const id = Date.now() + Math.random() // Unique ID for tracking
|
|
57
68
|
utterance.lang = options.lang || defaultUtteranceProps.lang
|
|
58
69
|
utterance.pitch = options.pitch || defaultUtteranceProps.pitch
|
|
59
70
|
utterance.rate = options.rate || defaultUtteranceProps.rate
|
|
60
71
|
utterance.voice = options.voice || defaultUtteranceProps.voice
|
|
61
72
|
utterance.volume = options.volume || defaultUtteranceProps.volume
|
|
73
|
+
utterances.set(id, utterance) // Strong reference
|
|
62
74
|
|
|
63
75
|
if (isAndroid === false) {
|
|
64
76
|
utterance.onstart = () => {
|
|
@@ -72,9 +84,14 @@ const speak = (options) => {
|
|
|
72
84
|
|
|
73
85
|
return new Promise((resolve, reject) => {
|
|
74
86
|
utterance.onend = () => {
|
|
87
|
+
clear()
|
|
88
|
+
utterances.delete(id) // Cleanup
|
|
75
89
|
resolve()
|
|
76
90
|
}
|
|
91
|
+
|
|
77
92
|
utterance.onerror = (e) => {
|
|
93
|
+
clear()
|
|
94
|
+
utterances.delete(id) // Cleanup
|
|
78
95
|
reject(e)
|
|
79
96
|
}
|
|
80
97
|
|
package/src/application.js
CHANGED
|
@@ -48,6 +48,7 @@ const Application = (config) => {
|
|
|
48
48
|
let keyUpHandler
|
|
49
49
|
let holdTimeout
|
|
50
50
|
let lastInputTime = 0
|
|
51
|
+
let lastInputKey = null
|
|
51
52
|
|
|
52
53
|
config.hooks[symbols.destroy] = function () {
|
|
53
54
|
document.removeEventListener('keydown', keyDownHandler)
|
|
@@ -66,8 +67,11 @@ const Application = (config) => {
|
|
|
66
67
|
const currentTime = performance.now()
|
|
67
68
|
|
|
68
69
|
const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode
|
|
70
|
+
const sameKey = lastInputKey === key
|
|
71
|
+
lastInputKey = key
|
|
69
72
|
// execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
|
|
70
|
-
|
|
73
|
+
// or key is different from the last used key
|
|
74
|
+
if (throttleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
|
|
71
75
|
return await processInput.call(this, e, key)
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -28,8 +28,14 @@ export default (component, state = () => {}) => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
const stateKeys = Object.keys(state.apply(component) || {})
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (stateKeys.indexOf('hasFocus') > -1) {
|
|
32
|
+
Log.warn(
|
|
33
|
+
'State `hasFocus` already exists as a built-in Component variable (to indicate whether the component currently has focus). Avoid using your own `hasFocus` key in the Component state'
|
|
34
|
+
)
|
|
35
|
+
} else {
|
|
36
|
+
// add built-in hasFocus key
|
|
37
|
+
stateKeys.push(['hasFocus'])
|
|
38
|
+
}
|
|
33
39
|
const stateKeysLength = stateKeys.length
|
|
34
40
|
|
|
35
41
|
for (let i = 0; i < stateKeysLength; i++) {
|
package/src/plugins/index.js
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* If not stated otherwise in this file or this component's LICENSE file the
|
|
3
|
+
* following copyright and licenses apply:
|
|
4
|
+
*
|
|
5
|
+
* Copyright 2019 Metrological
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the License);
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// LocalStorage and Cookie storage library
|
|
21
|
+
class localCookie {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
options = options || {}
|
|
24
|
+
this.forceCookies = options.forceCookies || false
|
|
25
|
+
|
|
26
|
+
if (this._checkIfLocalStorageWorks() === true && options.forceCookies !== true)
|
|
27
|
+
return {
|
|
28
|
+
getItem: this._getItemLocalStorage,
|
|
29
|
+
setItem: this._setItemLocalStorage,
|
|
30
|
+
removeItem: this._removeItemLocalStorage,
|
|
31
|
+
clear: this._clearLocalStorage,
|
|
32
|
+
keys: this._getLocalStorageKeys,
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
return {
|
|
36
|
+
getItem: this._getItemCookie,
|
|
37
|
+
setItem: this._setItemCookie,
|
|
38
|
+
removeItem: this._removeItemCookie,
|
|
39
|
+
clear: this._clearCookies,
|
|
40
|
+
keys: this._getCookieKeys,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Checks if local storage is available and works
|
|
45
|
+
_checkIfLocalStorageWorks() {
|
|
46
|
+
if (typeof localStorage !== 'undefined') {
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem('feature_test', 'yes')
|
|
49
|
+
if (localStorage.getItem('feature_test') === 'yes') {
|
|
50
|
+
localStorage.removeItem('feature_test')
|
|
51
|
+
// localStorage is enabled
|
|
52
|
+
return true
|
|
53
|
+
} else {
|
|
54
|
+
// localStorage is disabled
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// localStorage is disabled
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// localStorage is not available
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Local Storage APIs
|
|
69
|
+
*/
|
|
70
|
+
_getItemLocalStorage(key) {
|
|
71
|
+
return window.localStorage.getItem(key)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_setItemLocalStorage(key, value) {
|
|
75
|
+
return window.localStorage.setItem(key, value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_removeItemLocalStorage(key) {
|
|
79
|
+
return window.localStorage.removeItem(key)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_clearLocalStorage() {
|
|
83
|
+
return window.localStorage.clear()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_getLocalStorageKeys() {
|
|
87
|
+
return Object.keys(window.localStorage)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* Cookie APIs
|
|
92
|
+
*/
|
|
93
|
+
_getItemCookie(key) {
|
|
94
|
+
/* eslint-disable */
|
|
95
|
+
function escape(s) { return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, '\\$1'); };
|
|
96
|
+
var match = document.cookie.match(RegExp('(?:^|;\\s*)' + escape(key) + '=([^;]*)'));
|
|
97
|
+
|
|
98
|
+
//if value in key of the entire cookie is empty, its deleted
|
|
99
|
+
if (match && match[1] === '')
|
|
100
|
+
match[1] = null
|
|
101
|
+
|
|
102
|
+
return match ? match[1] : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_setItemCookie(key, value) {
|
|
106
|
+
var now = new Date()
|
|
107
|
+
var expires = new Date(now.getTime() +1000*60*60*24*365*5) // 5 years!
|
|
108
|
+
document.cookie = `${key}=${value}; expires=${expires.toUTCString()};`
|
|
109
|
+
return //return undefined just like localStorage
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_removeItemCookie(key) {
|
|
113
|
+
// to delete an k/v set the key to empty
|
|
114
|
+
document.cookie = `${key}=;Max-Age=-99999999;`
|
|
115
|
+
return //return undefined just like localStorage
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_clearCookies() {
|
|
119
|
+
// empty all key/values in cookies
|
|
120
|
+
// What about cookies we got from the server? ¯\_(ツ)_/¯
|
|
121
|
+
// They should have the HttpOnly flag, so we shouldn't be able to delete them
|
|
122
|
+
document.cookie.split(";").forEach( (c) => {
|
|
123
|
+
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=Max-Age=-99999999");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return //return undefined
|
|
127
|
+
}
|
|
128
|
+
_getCookieKeys() {
|
|
129
|
+
return document.cookie.split(";").map((item)=>item.split("=")[0])
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default localCookie
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Comcast Cable Communications Management, LLC
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*
|
|
15
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import localCookie from './localCookie.js'
|
|
19
|
+
|
|
20
|
+
let lc = new localCookie()
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
name: 'storage',
|
|
24
|
+
plugin() {
|
|
25
|
+
return {
|
|
26
|
+
get(key) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(lc.getItem(key))
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
set(key, value) {
|
|
34
|
+
try {
|
|
35
|
+
lc.setItem(key, JSON.stringify(value))
|
|
36
|
+
return true
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
remove(key) {
|
|
42
|
+
lc.removeItem(key)
|
|
43
|
+
},
|
|
44
|
+
clear() {
|
|
45
|
+
lc.clear()
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|