@lightningjs/blits 0.6.0 → 0.6.2

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ # v0.6.2
4
+
5
+ _12 jan 2024_
6
+
7
+ - Fixed issue with Sprites not working correctly due to missing background color
8
+
9
+ # v0.6.1
10
+
11
+ _8 jan 2024_
12
+
13
+ - Fixed issue with using single quotes as Text content
14
+ - Added first Text-to-Speech / Announcer functionality
15
+
3
16
  # v0.6.0
4
17
 
5
18
  _4 jan 2024_
@@ -31,6 +31,6 @@
31
31
  "vite": "^4.0.4"
32
32
  },
33
33
  "dependencies": {
34
- "@lightningjs/blits": "^0.6.0"
34
+ "@lightningjs/blits": "^0.6.2"
35
35
  }
36
36
  }
package/docs/_sidebar.md CHANGED
@@ -9,4 +9,5 @@
9
9
  - [Watching for changes](watchers.md)
10
10
  - [Styling and positioning Elements](element_attributes.md)
11
11
  - [Displaying images](displaying_images.md)
12
+ - [Text-to-Speech / Announcer](text-to-speech-announcer.md)
12
13
 
@@ -0,0 +1,50 @@
1
+ # Blits - Lightning 3 App Development Framework
2
+
3
+ ## Text-to-Speech / Announcer
4
+
5
+ Blits comes with a built-in Text-to-Speech / Announcer functionality, to easily make your App more accessible. This is something that's often overlooked when developing an App, but having speech assistance integrated into your App is a huge deal for those users that are visually impaired.
6
+
7
+ ### Integrated with the router
8
+
9
+ The Announcer in Blits is directly integrated with the router functionality. This means that through simple configuration, a descriptive message can be announced, when navigating from page to page, without the need for adding extra custom code.
10
+
11
+ Just add an `announce` key in the configuration of your route, that contains the message you would like to be announced when visiting that route.
12
+
13
+ ```js
14
+ const routes = [
15
+ // ...
16
+ {
17
+ path: '/settings',
18
+ component: Settings,
19
+ announce: "Welcome to the Settings page",
20
+ },
21
+ // ...
22
+ ]
23
+ ```
24
+
25
+ ## Custom announcement messages
26
+
27
+ Furthermore it's possible to speak out custom messages anywhere in your App logic, for example when a component receives focus, as a reaction to user input, or to announce error messages when an remote API call
28
+ fails.
29
+
30
+ Each component in Blits has a `this.$announcer.speak()` available, which accepts `string` of a message to be announced.
31
+
32
+ ```js
33
+ Blits.Component('MyComponent', {
34
+ // ...
35
+ hooks: {
36
+ focus() {
37
+ this.$announcer.speak('MyComponent just got focus')
38
+ }
39
+ },
40
+ keys: {
41
+ right() {
42
+ if(this.focused === this.items.length - 1) {
43
+ this.$announcer.speak('End of row reached')
44
+ } else {
45
+ //
46
+ }
47
+ }
48
+ }
49
+ })
50
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.cjs",
6
6
  "exports": {
@@ -0,0 +1,48 @@
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 speechSynthesis from './speechSynthesis.js'
19
+
20
+ let debounce
21
+
22
+ const speak = (message, politeness = 'off') => {
23
+ clearTimeout(debounce)
24
+ speechSynthesis.cancel()
25
+ // assertive messages get spoken immediately
26
+ if (politeness === 'assertive') {
27
+ speechSynthesis.speak({ value: message })
28
+ } else {
29
+ debounce = setTimeout(() => {
30
+ speechSynthesis.speak({ value: message })
31
+ }, 400)
32
+ }
33
+ }
34
+
35
+ const polite = (message) => speak(message, 'polite')
36
+
37
+ const assertive = (message) => speak(message, 'assertive')
38
+
39
+ const stop = () => {
40
+ speechSynthesis.cancel()
41
+ }
42
+
43
+ export default {
44
+ speak,
45
+ polite,
46
+ assertive,
47
+ stop,
48
+ }
@@ -0,0 +1,127 @@
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
+ const syn = window.speechSynthesis
19
+
20
+ const getUA = () => (window.navigator || {}).userAgent || ''
21
+ const isAndroid = () => /android/i.test(getUA())
22
+
23
+ let initialized = false
24
+ let infinityTimer = null
25
+ const clear = () => infinityTimer && clearTimeout(infinityTimer)
26
+
27
+ const resumeInfinity = (target) => {
28
+ if (!target || infinityTimer) {
29
+ return clear()
30
+ }
31
+
32
+ syn.pause()
33
+ syn.resume()
34
+
35
+ infinityTimer = setTimeout(() => {
36
+ resumeInfinity(target)
37
+ }, 5000)
38
+ }
39
+
40
+ const utterProps = {
41
+ lang: 'en-US',
42
+ pitch: 1,
43
+ rate: 1,
44
+ voice: null,
45
+ volume: 1,
46
+ }
47
+
48
+ const initialize = () => {
49
+ utterProps.voice = syn.getVoices()[0]
50
+ initialized = true
51
+ }
52
+
53
+ const utterance = (scope, e) => {
54
+ const utter = new SpeechSynthesisUtterance(e.value)
55
+
56
+ // utter props
57
+ utter.lang = e.lang || utterProps.lang
58
+ utter.pitch = e.pitch || utterProps.pitch
59
+ utter.rate = e.rate || utterProps.rate
60
+ utter.voice = e.voice || utterProps.voice
61
+ utter.volume = e.volume || utterProps.volume
62
+
63
+ // utter events
64
+ utter.onstart = () => {
65
+ if (!isAndroid()) {
66
+ resumeInfinity(utter)
67
+ }
68
+ scope.onstart()
69
+ }
70
+
71
+ utter.onresume = () => {
72
+ if (!isAndroid()) {
73
+ resumeInfinity(utter)
74
+ }
75
+ scope.onresume()
76
+ }
77
+
78
+ utter.onpause = () => {
79
+ scope.onpause()
80
+ }
81
+ utter.onend = () => {
82
+ scope.onend()
83
+ }
84
+ utter.onerror = () => {
85
+ clear()
86
+ scope.onerror()
87
+ }
88
+ syn.speak(utter)
89
+ }
90
+
91
+ export default {
92
+ speak(e) {
93
+ if (!initialized) {
94
+ initialize()
95
+ }
96
+ this.cancel()
97
+ utterance(this, e)
98
+ },
99
+ resume() {
100
+ syn.resume()
101
+ },
102
+ pause() {
103
+ syn.pause()
104
+ },
105
+ cancel() {
106
+ syn.cancel()
107
+ clear()
108
+ },
109
+ getVoices() {
110
+ return syn.getVoices()
111
+ },
112
+ onend() {
113
+ //event placeholder
114
+ },
115
+ onerror() {
116
+ //event placeholder
117
+ },
118
+ onstart() {
119
+ //event placeholder
120
+ },
121
+ onresume() {
122
+ //event placeholder
123
+ },
124
+ onpause() {
125
+ //eventplaceholder
126
+ },
127
+ }
@@ -70,6 +70,19 @@ declare namespace Application {
70
70
 
71
71
  type RouteTransitionFunction = (previousRoute: Route, currentRoute: Route) => RequireAtLeastOne<RouteTransition>
72
72
 
73
+ interface RouteAnnounce {
74
+ /**
75
+ * Message to be announced
76
+ */
77
+ message: String,
78
+ /**
79
+ * Politeness level
80
+ *
81
+ * Defaults to 'off'
82
+ */
83
+ politeness?: 'off' | 'polite' | 'assertive'
84
+ }
85
+
73
86
  function Application(
74
87
  config: Application.ApplicationConfig
75
88
  ) : Application.ApplicationInstance
@@ -94,7 +107,14 @@ declare namespace Application {
94
107
  /**
95
108
  * Extra route options
96
109
  */
97
- options?: object // todo: specify which options are available
110
+ options?: object // todo: specify which options are available,
111
+ /**
112
+ * Message to be announced when visiting the route (often used for accessibility purposes)
113
+ *
114
+ * Can be either a `String` with the message or an object that defines the message and the
115
+ * politeness level
116
+ */
117
+ announce?: String | RouteAnnounce
98
118
  }
99
119
 
100
120
  export interface ApplicationInstance extends ComponentInstance {}
@@ -22,7 +22,7 @@ import symbols from '../lib/symbols.js'
22
22
  export default () =>
23
23
  Component('Sprite', {
24
24
  template: `
25
- <Element w="$w" h="$h" :texture="$texture" />
25
+ <Element w="$w" h="$h" color="#fff" :texture="$texture" />
26
26
  `,
27
27
  props: ['image', 'map', 'frame', 'w', 'h'],
28
28
  state() {
@@ -32,6 +32,7 @@ export default () =>
32
32
  },
33
33
  computed: {
34
34
  texture() {
35
+ console.log('texture')
35
36
  const options =
36
37
  'frames' in this.map
37
38
  ? { ...(this.map.defaults || {}), ...this.map.frames[this.frame] }
@@ -50,6 +51,7 @@ export default () =>
50
51
  },
51
52
  hooks: {
52
53
  ready() {
54
+ console.log('sprite!')
53
55
  this.spriteTexture = this[symbols.renderer].createTexture('ImageTexture', {
54
56
  src: `${window.location.protocol}//${window.location.host}/${this.image}`,
55
57
  })
@@ -316,7 +316,8 @@ const cast = (val = '', key = false, component = 'component.') => {
316
316
  if (val.startsWith('$')) {
317
317
  castedValue = `${component}${val.replace('$', '')}`
318
318
  } else {
319
- castedValue = `'${parseInlineContent(val, component)}'`
319
+ // unescaped single quotes must be escaped
320
+ castedValue = `'${parseInlineContent(val.replace(/(?<!\\)'/g, "\\'"), component)}'`
320
321
  }
321
322
  }
322
323
  // numeric
@@ -28,6 +28,7 @@ import { default as log, Log } from '../log.js'
28
28
  import symbols from '../symbols.js'
29
29
 
30
30
  import { trigger } from '../reactivity/effect.js'
31
+ import Announcer from '../../announcer/announcer.js'
31
32
 
32
33
  const shaderAlias = {
33
34
  rounded: 'radius',
@@ -222,6 +223,12 @@ export default (component) => {
222
223
  enumerable: false,
223
224
  configurable: false,
224
225
  },
226
+ $announcer: {
227
+ value: Announcer,
228
+ writable: false,
229
+ enumerable: false,
230
+ configurable: false,
231
+ },
225
232
  })
226
233
  }
227
234
 
@@ -21,6 +21,7 @@ import symbols from '../lib/symbols.js'
21
21
  import { Log } from '../lib/log.js'
22
22
  import Element from '../element.js'
23
23
  import Focus from '../focus.js'
24
+ import Announcer from '../announcer/announcer.js'
24
25
 
25
26
  export let currentRoute
26
27
  export let navigating = false
@@ -55,7 +56,7 @@ export const navigate = async function () {
55
56
  const route = matchHash(hash, this.parent[symbols.routes])
56
57
 
57
58
  if (route) {
58
- // add the previous route (tecnhically still the current route at this point)
59
+ // add the previous route (technically still the current route at this point)
59
60
  // into the history stack, unless navigating back or inHistory flag of route is false
60
61
  if (navigatingBack === false && previousRoute && previousRoute.options.inHistory === true) {
61
62
  history.push(previousRoute)
@@ -132,6 +133,16 @@ export const navigate = async function () {
132
133
  }
133
134
  }
134
135
 
136
+ // Announce route change if a message has been specified for this route
137
+ if (route.announce) {
138
+ if (typeof route.announce === 'string') {
139
+ route.announce = {
140
+ message: route.announce,
141
+ }
142
+ }
143
+ Announcer.speak(route.announce.message, route.announce.politeness)
144
+ }
145
+
135
146
  // focus the new view
136
147
  focus ? Focus.set(focus) : view.focus()
137
148
  this.activeView = this[symbols.children][this[symbols.children].length - 1]