|
17 | 17 | * limitations under the License.
|
18 | 18 | */
|
19 | 19 |
|
20 |
| -import Speech from './Speech.js' |
| 20 | +import SpeechEngine from './Speech.js' |
21 | 21 | import { debounce, getElmName } from './utils.js'
|
22 | 22 |
|
23 |
| -const defaultOptions = { |
24 |
| - voiceOutDelay: 500, |
25 |
| - announcerFocusDebounce: 400, |
26 |
| - announcerTimeout: 5 * 60 * 1000, //Five Minutes |
27 |
| -} |
28 |
| - |
29 |
| -export default function Announcer(Base, speak = Speech, announcerOptions = {}) { |
30 |
| - const options = { ...defaultOptions, ...announcerOptions } |
31 |
| - |
32 |
| - return class extends Base { |
33 |
| - _construct() { |
34 |
| - this._debounceAnnounceFocusChanges = debounce( |
35 |
| - this._announceFocusChanges.bind(this), |
36 |
| - options.announcerFocusDebounce |
37 |
| - ) |
| 23 | +let resetFocusTimer; |
| 24 | +let prevFocusPath = []; |
| 25 | +let currentlySpeaking; |
| 26 | +let voiceOutDisabled = false; |
| 27 | +const fiveMinutes = 300000; |
38 | 28 |
|
39 |
| - this._resetFocusTimer = debounce(() => { |
40 |
| - // Reset focus path for full announce |
41 |
| - this._lastFocusPath = undefined |
42 |
| - }, options.announcerTimeout) |
43 |
| - |
44 |
| - // Lightning only calls Focus Change on second focus |
45 |
| - this._focusChange() |
46 |
| - } |
47 |
| - |
48 |
| - _voiceOut(toSpeak) { |
49 |
| - if (this._voiceOutDisabled) { |
50 |
| - return |
51 |
| - } |
| 29 | +function onFocusChangeCore(focusPath = []) { |
| 30 | + if (!Announcer.enabled) { |
| 31 | + return |
| 32 | + } |
52 | 33 |
|
53 |
| - const speech = speak(toSpeak) |
54 |
| - // event using speech synthesis api promise |
55 |
| - if (speech && speech.series) { |
56 |
| - speech.series.then(() => { |
57 |
| - this.stage.emit('announceEnded') |
58 |
| - }) |
59 |
| - } |
| 34 | + const loaded = focusPath.every(elm => !elm.loading) |
| 35 | + const focusDiff = focusPath.filter(elm => !prevFocusPath.includes(elm)) |
60 | 36 |
|
61 |
| - // event in case speech synthesis api is flakey, |
62 |
| - // assume the ammount of time it takes to read each word |
63 |
| - const toAnnounceStr = Array.isArray(toSpeak) ? toSpeak.concat().join(' ') : toSpeak |
64 |
| - const toAnnounceWords = toAnnounceStr.split(' ') |
65 |
| - const timeoutDelay = toAnnounceWords.length * options.voiceOutDelay |
66 |
| - clearTimeout(this._announceEndedTimeout) |
67 |
| - this._announceEndedTimeout = setTimeout(() => { |
68 |
| - this.stage.emit('announceTimeoutEnded') |
69 |
| - }, timeoutDelay) |
70 |
| - |
71 |
| - return speech |
72 |
| - } |
| 37 | + resetFocusTimer() |
| 38 | + Announcer.cancel() |
73 | 39 |
|
74 |
| - _disable() { |
75 |
| - clearTimeout(this._announceEndedTimeout) |
76 |
| - this.stage.emit('announceEnded') |
77 |
| - this.stage.emit('announceTimeoutEnded') |
78 |
| - } |
| 40 | + if (!loaded) { |
| 41 | + Announcer.onFocusChange() |
| 42 | + return |
| 43 | + } |
79 | 44 |
|
80 |
| - set announcerEnabled(val) { |
81 |
| - this._announcerEnabled = val |
82 |
| - this._focusChange() |
83 |
| - } |
| 45 | + prevFocusPath = focusPath.slice(0) |
84 | 46 |
|
85 |
| - get announcerEnabled() { |
86 |
| - return this._announcerEnabled |
| 47 | + let toAnnounceText = []; |
| 48 | + let toAnnounce = focusDiff.reduce((acc, elm) => { |
| 49 | + if (elm.announce) { |
| 50 | + acc.push([getElmName(elm), 'Announce', elm.announce]) |
| 51 | + toAnnounceText.push(elm.announce) |
| 52 | + } else if (elm.title) { |
| 53 | + acc.push([getElmName(elm), 'Title', elm.title]) |
| 54 | + toAnnounceText.push(elm.title) |
87 | 55 | }
|
88 |
| - |
89 |
| - _focusChange() { |
90 |
| - if (!this._resetFocusTimer) { |
91 |
| - return |
92 |
| - } |
93 |
| - |
94 |
| - this._resetFocusTimer() |
95 |
| - this.$announcerCancel() |
96 |
| - this._debounceAnnounceFocusChanges() |
| 56 | + return acc |
| 57 | + }, []) |
| 58 | + |
| 59 | + focusDiff.reverse().reduce((acc, elm) => { |
| 60 | + if (elm.announceContext) { |
| 61 | + acc.push([getElmName(elm), 'Context', elm.announceContext]) |
| 62 | + toAnnounceText.push(elm.announceContext) |
| 63 | + } else { |
| 64 | + acc.push([getElmName(elm), 'No Context', '']) |
97 | 65 | }
|
| 66 | + return acc |
| 67 | + }, toAnnounce) |
98 | 68 |
|
99 |
| - _announceFocusChanges() { |
100 |
| - const focusPath = this.application.focusPath || [] |
101 |
| - const lastFocusPath = this._lastFocusPath || [] |
102 |
| - const loaded = focusPath.every(elm => !elm.loading) |
103 |
| - const focusDiff = focusPath.filter(elm => !lastFocusPath.includes(elm)) |
104 |
| - |
105 |
| - if (!loaded) { |
106 |
| - this._debounceAnnounceFocusChanges() |
107 |
| - return |
108 |
| - } |
109 |
| - |
110 |
| - this._lastFocusPath = focusPath.slice(0) |
111 |
| - // Provide hook for focus diff for things like TextBanner |
112 |
| - this.focusDiffHook = focusDiff |
113 |
| - |
114 |
| - if (!this.announcerEnabled) { |
115 |
| - return |
116 |
| - } |
117 |
| - |
118 |
| - let toAnnounce = focusDiff.reduce((acc, elm) => { |
119 |
| - if (elm.announce) { |
120 |
| - acc.push([getElmName(elm), 'Announce', elm.announce]) |
121 |
| - } else if (elm.title) { |
122 |
| - acc.push([getElmName(elm), 'Title', elm.title || '']) |
123 |
| - } |
124 |
| - return acc |
125 |
| - }, []) |
126 |
| - |
127 |
| - focusDiff.reverse().reduce((acc, elm) => { |
128 |
| - if (elm.announceContext) { |
129 |
| - acc.push([getElmName(elm), 'Context', elm.announceContext]) |
130 |
| - } else { |
131 |
| - acc.push([getElmName(elm), 'No Context', '']) |
132 |
| - } |
133 |
| - return acc |
134 |
| - }, toAnnounce) |
135 |
| - |
136 |
| - if (this.debug) { |
137 |
| - console.table(toAnnounce) |
138 |
| - } |
| 69 | + if (Announcer.debug) { |
| 70 | + console.table(toAnnounce) |
| 71 | + } |
139 | 72 |
|
140 |
| - toAnnounce = toAnnounce.reduce((acc, a) => { |
141 |
| - const txt = a[2] |
142 |
| - txt && acc.push(txt) |
143 |
| - return acc |
144 |
| - }, []) |
145 |
| - |
146 |
| - if (toAnnounce.length) { |
147 |
| - this.$announcerCancel() |
148 |
| - this._currentlySpeaking = this._voiceOut( |
149 |
| - toAnnounce.reduce((acc, val) => acc.concat(val), []) |
150 |
| - ) |
151 |
| - } |
152 |
| - } |
| 73 | + if (toAnnounceText.length) { |
| 74 | + Announcer.cancel() |
| 75 | + return currentlySpeaking = Announcer._textToSpeech( |
| 76 | + toAnnounceText.reduce((acc, val) => acc.concat(val), []) |
| 77 | + ) |
| 78 | + } |
| 79 | +} |
153 | 80 |
|
154 |
| - $announce(toAnnounce, { append = false, notification = false } = {}) { |
155 |
| - if (this.announcerEnabled) { |
156 |
| - this._debounceAnnounceFocusChanges.flush() |
157 |
| - if (append && this._currentlySpeaking && this._currentlySpeaking.active) { |
158 |
| - this._currentlySpeaking.append(toAnnounce) |
| 81 | +const Announcer = { |
| 82 | + enabled: true, |
| 83 | + debug: false, |
| 84 | + cancel: function() { |
| 85 | + currentlySpeaking && currentlySpeaking.cancel() |
| 86 | + }, |
| 87 | + refresh: function(depth = 0) { |
| 88 | + prevFocusPath = prevFocusPath.slice(0, depth) |
| 89 | + resetFocusTimer() |
| 90 | + }, |
| 91 | + speak: function(text, { append = false, notification = false } = {}) { |
| 92 | + if (Announcer.enabled) { |
| 93 | + Announcer.onFocusChange.flush() |
| 94 | + if (append && currentlySpeaking && currentlySpeaking.active) { |
| 95 | + currentlySpeaking.append(text) |
159 | 96 | } else {
|
160 |
| - this.$announcerCancel() |
161 |
| - this._currentlySpeaking = this._voiceOut(toAnnounce) |
| 97 | + Announcer.cancel() |
| 98 | + currentlySpeaking = Announcer._textToSpeech(text) |
162 | 99 | }
|
163 | 100 |
|
164 | 101 | if (notification) {
|
165 |
| - this._voiceOutDisabled = true |
166 |
| - this._currentlySpeaking.series.finally(() => { |
167 |
| - this._voiceOutDisabled = false |
168 |
| - this.$announcerRefresh() |
| 102 | + voiceOutDisabled = true |
| 103 | + currentlySpeaking.series.finally(() => { |
| 104 | + voiceOutDisabled = false |
| 105 | + Announcer.refresh() |
169 | 106 | })
|
170 | 107 | }
|
171 | 108 | }
|
172 |
| - } |
173 | 109 |
|
174 |
| - $announcerCancel() { |
175 |
| - this._currentlySpeaking && this._currentlySpeaking.cancel() |
| 110 | + return currentlySpeaking |
| 111 | + }, |
| 112 | + _textToSpeech(toSpeak) { |
| 113 | + if (voiceOutDisabled) { |
| 114 | + return |
176 | 115 | }
|
177 | 116 |
|
178 |
| - $announcerRefresh(depth = 0) { |
179 |
| - this._lastFocusPath = this._lastFocusPath.slice(0, depth) |
180 |
| - this._resetFocusTimer() |
181 |
| - this._focusChange() |
182 |
| - } |
| 117 | + return SpeechEngine(toSpeak) |
| 118 | + }, |
| 119 | + init: function(focusDebounce = 400, focusChangeTimeout = fiveMinutes) { |
| 120 | + Announcer.onFocusChange = debounce( |
| 121 | + onFocusChangeCore, |
| 122 | + focusDebounce |
| 123 | + ); |
| 124 | + |
| 125 | + resetFocusTimer = debounce(() => { |
| 126 | + // Reset focus path for full announce |
| 127 | + prevFocusPath = [] |
| 128 | + }, focusChangeTimeout) |
183 | 129 | }
|
184 | 130 | }
|
| 131 | +Announcer.init(); |
| 132 | +export default Announcer; |
0 commit comments