Updated cards from tuesch changes. Incorporated InteractionMapper memory leak fix.
This commit is contained in:
+189
-115
@@ -146,7 +146,6 @@ CardPlugin.EnlargeableThumbnail = class EnlargeableThumbnail extends CardPluginB
|
||||
this.fadeAnimationDuration = fadeAnimationDuration
|
||||
this.interactionType = interactionType
|
||||
}
|
||||
|
||||
get require() {
|
||||
return [CardPlugin.LightBox]
|
||||
}
|
||||
@@ -392,126 +391,201 @@ CardPlugin.Speech = class SpeechPlugin extends CardPluginBase {
|
||||
this.className = className
|
||||
this.parentSelector = parentSelector
|
||||
this.interactionType = interactionType
|
||||
}
|
||||
|
||||
get require() {
|
||||
return [CardPlugin.Ui]
|
||||
}
|
||||
|
||||
|
||||
append(context) {
|
||||
let container = context.querySelector(this.parentSelector)
|
||||
this.button = document.createElement('div')
|
||||
this.button.className = 'icon button ' + this.className
|
||||
container.appendChild(this.button)
|
||||
|
||||
InteractionMapper.on(this.interactionType, this.button, () => {
|
||||
let subcard = context.querySelector('.mainview > .subcard')
|
||||
let target = subcard ? subcard : context
|
||||
|
||||
this.speak(target)
|
||||
})
|
||||
}
|
||||
|
||||
_activate() {
|
||||
this._disableActive()
|
||||
this.active = this
|
||||
this._activateButton()
|
||||
}
|
||||
|
||||
_activateButton() {
|
||||
if (this.button) this.button.classList.add('active')
|
||||
}
|
||||
|
||||
_deactivate() {
|
||||
this._deactivateButton()
|
||||
}
|
||||
|
||||
_deactivateButton() {
|
||||
if (this.button) this.button.classList.remove('active')
|
||||
}
|
||||
|
||||
_isSameNode(node) {
|
||||
//console.log(this.currentText, node.innerText)
|
||||
return this.currentText == node.innerText
|
||||
}
|
||||
|
||||
speak(node) {
|
||||
console.log(this._isSameNode(node))
|
||||
|
||||
if (!window.speechSynthesis.speaking) {
|
||||
console.log('Noone talking!')
|
||||
this._start(node)
|
||||
} else if (this._isSameNode(node)) {
|
||||
console.log('Requested same!')
|
||||
this._stop()
|
||||
} else {
|
||||
console.log('Requested Different!')
|
||||
this._stop()
|
||||
this._start(node)
|
||||
}
|
||||
}
|
||||
|
||||
_disableActive() {
|
||||
console.log('disableActive:', this.active)
|
||||
if (this.active) {
|
||||
this.active._deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
_start(node) {
|
||||
this.currentText = node.innerText
|
||||
let utterance = new SpeechSynthesisUtterance(node.innerText)
|
||||
|
||||
let voices = window.speechSynthesis.getVoices()
|
||||
console.log(voices)
|
||||
let voice = voices.filter(val => {
|
||||
//console.log(val)
|
||||
return val.name == 'Microsoft Hedda Desktop - German'
|
||||
})[0]
|
||||
|
||||
//console.log(voice)
|
||||
|
||||
utterance.voice = voice
|
||||
console.log('TALK: ', utterance)
|
||||
window.speechSynthesis.speak(utterance)
|
||||
this._activate()
|
||||
window.speechSynthesis.resume()
|
||||
|
||||
utterance.onboundary = () => {
|
||||
console.log('onboundary', node.innerText)
|
||||
if (this.currentText.substring(0, 5) != node.innerText.substring(0, 5)) {
|
||||
console.log('text for speech synth changed!', this.currentText, node.innerText)
|
||||
this._stop()
|
||||
}
|
||||
}
|
||||
utterance.onend = () => console.log('onend', node.innerText)
|
||||
utterance.onerror = () => console.log('onerror', node.innerText)
|
||||
utterance.onmark = () => console.log('onmark', node.innerText)
|
||||
utterance.onpause = () => console.log('onpause', node.innerText)
|
||||
utterance.onresume = () => console.log('onresume', node.innerText)
|
||||
utterance.onstart = () => console.log('onstart', node.innerText)
|
||||
utterance.onerror = () => console.log('onerror', node.innerText)
|
||||
}
|
||||
|
||||
_stop() {
|
||||
/*
|
||||
Speech doesn't stop when page is navigated.
|
||||
Therefore we do it manually here.
|
||||
*/
|
||||
window.addEventListener('beforeunload', () => {
|
||||
window.speechSynthesis.cancel()
|
||||
this.currentText = null
|
||||
this._deactivate()
|
||||
}
|
||||
})
|
||||
|
||||
get active() {
|
||||
return this.constructor.active
|
||||
}
|
||||
// Binding the function beforehand ensures, that the end function is always the same.
|
||||
this._end = this._end.bind(this)
|
||||
|
||||
set active(val) {
|
||||
this.constructor.active = val
|
||||
}
|
||||
this._setupUtterance()
|
||||
this.utterance.addEventListener('end', event => {
|
||||
this._end()
|
||||
})
|
||||
}
|
||||
|
||||
get currentText() {
|
||||
return this.constructor.text
|
||||
}
|
||||
get require() {
|
||||
return [CardPlugin.Ui]
|
||||
}
|
||||
|
||||
set currentText(val) {
|
||||
this.constructor.text = val
|
||||
subcardChanged(closed) {
|
||||
if (this.cardActive) {
|
||||
this._updateText(closed)
|
||||
}
|
||||
}
|
||||
|
||||
get cardActive() {
|
||||
return this.activeUtterance == this.utterance
|
||||
}
|
||||
|
||||
_updateText(ignoreSubcard = false) {
|
||||
let node = this.context
|
||||
let subcard = this.context.querySelector('.mainview > .subcard')
|
||||
|
||||
if (ignoreSubcard) {
|
||||
if (subcard != null) {
|
||||
let clone = node.cloneNode(true)
|
||||
let clonedSubcard = clone.querySelector('.mainview > .subcard')
|
||||
clonedSubcard.parentNode.removeChild(clonedSubcard)
|
||||
node = clone
|
||||
}
|
||||
} else {
|
||||
if (subcard) {
|
||||
let clone = subcard.cloneNode(true)
|
||||
clone.querySelectorAll('figure').forEach(figure => {
|
||||
figure.parentNode.removeChild(figure)
|
||||
})
|
||||
|
||||
node = clone
|
||||
}
|
||||
}
|
||||
|
||||
let id = this.context.getAttribute('data-id')
|
||||
let src = this.context.getAttribute('data-source')
|
||||
let subcardSource = null
|
||||
if (subcard != null) {
|
||||
subcardSource = subcard.getAttribute('data-source')
|
||||
}
|
||||
|
||||
if (!window.speechSynthesis.speaking) {
|
||||
this._start(node)
|
||||
Logging.log(`Started speech on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
|
||||
} else if (this.cardActive && this._sameText(node)) {
|
||||
Logging.log(`Stopped speech on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
|
||||
this._stop()
|
||||
} else {
|
||||
Logging.log(`Updated Text on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
|
||||
this._stop()
|
||||
.then(() => {
|
||||
this._start(node)
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
_sameText(node) {
|
||||
return this.utterance.text == this._cleanupText(node)
|
||||
}
|
||||
|
||||
_setupUtterance() {
|
||||
this.utterance = new SpeechSynthesisUtterance()
|
||||
this.utterance.lang = 'de-DE'
|
||||
}
|
||||
|
||||
get require() {
|
||||
return [CardPlugin.Ui]
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.button = null
|
||||
super.remove()
|
||||
}
|
||||
|
||||
append(context) {
|
||||
let container = context.querySelector(this.parentSelector)
|
||||
this.button = document.createElement('div')
|
||||
this.button.className = 'icon button ' + this.className
|
||||
container.appendChild(this.button)
|
||||
|
||||
InteractionMapper.on(this.interactionType, this.button, () => {
|
||||
this.speak()
|
||||
})
|
||||
|
||||
this.context.addEventListener('DOMNodeRemoved', event => {
|
||||
if (
|
||||
this.context['lastSpeechNode'] == window.speechSynthesis['speechPluginNode'] &&
|
||||
event.target == this.context
|
||||
) {
|
||||
this._stop()
|
||||
}
|
||||
})
|
||||
|
||||
ScatterCard
|
||||
}
|
||||
|
||||
_isSameNode(node) {
|
||||
return this.currentText == node.textContent
|
||||
}
|
||||
|
||||
speak() {
|
||||
/**
|
||||
* This is a little bit ugly, but imho the most elegant of all dirty solutions.
|
||||
*
|
||||
5ht * Within the plugins we have no knowledge of other cards and such. But must differentiate the
|
||||
* clicks by their corresponding owner. The SpeechUtterance just takes a text and has no knowledge
|
||||
* about the node that is currently read to the user.
|
||||
*
|
||||
* This means, that we can identify same text, but not differentiate same text on different nodes.
|
||||
* To account for that, we add the node to the speechSynthesis object (#benefitsOfJavaScript) and
|
||||
* have access to the node, by - let's say - expanding the functionality of the SpeechSynthesis object.
|
||||
*
|
||||
* SO -17.07.19
|
||||
*/
|
||||
|
||||
let activeNode = window.speechSynthesis['speechPluginNode']
|
||||
this._updateText()
|
||||
}
|
||||
|
||||
async _stop() {
|
||||
return new Promise(resolve => {
|
||||
if (this.activeUtterance) {
|
||||
this.activeUtterance.addEventListener('end', resolve, {
|
||||
once: true
|
||||
})
|
||||
}
|
||||
|
||||
window.speechSynthesis.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
get activeUtterance() {
|
||||
return window.speechSynthesis['speechPluginUtterance']
|
||||
}
|
||||
|
||||
_end() {
|
||||
window.speechSynthesis['speechPluginNode'] = null
|
||||
window.speechSynthesis['speechPluginUtterance'] = null
|
||||
this._deactivateButton()
|
||||
this.context.classList.remove('speech-plugin-is-reading')
|
||||
}
|
||||
|
||||
_start(node) {
|
||||
window.speechSynthesis.cancel()
|
||||
|
||||
window.speechSynthesis['speechPluginUtterance'] = this.utterance
|
||||
window.speechSynthesis['speechPluginNode'] = node
|
||||
this.context['lastSpeechNode'] = node
|
||||
|
||||
let cleanText = this._cleanupText(node)
|
||||
this.utterance.text = cleanText
|
||||
window.speechSynthesis.speak(this.utterance)
|
||||
this._activateButton()
|
||||
|
||||
this.context.classList.add('speech-plugin-is-reading')
|
||||
}
|
||||
|
||||
_cleanupText(node) {
|
||||
let text = node.textContent
|
||||
text = this._removeShy(text)
|
||||
return text
|
||||
}
|
||||
|
||||
_removeShy(text) {
|
||||
return text.replace(/\u00AD/g, '')
|
||||
}
|
||||
|
||||
_activateButton() {
|
||||
if (this.button) this.button.classList.add('active')
|
||||
}
|
||||
_deactivateButton() {
|
||||
if (this.button) this.button.classList.remove('active')
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user