.wrap
h1 Talker
#app
//- .box.block(aria-label="error", hidden)
//- p Sorry, your browser doesn't support <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/speechSynthesis">Speech Synthesis</a>.
.box.block(aria-label="instructions")
h2 Instructions
ul
li Press 's' or 'Enter' to play the current text in the textbox.
li Press 'x' to cancel the current playback.
li Shift + click on the close icon (x) on phrases to remove them without the confirmation dialog.
li Shift + click on a voice button to select that voice and also play the phrase currently in the text box.
//- li Press 'p' to pause the current playback.
//- li Press 'r' to resume the current playback.
.box.block(aria-label="voices")
h2 Voices
.box.current-voice
.voices
.box.block(aria-label="phrases")
h2 Phrases
label(for="phrase") Enter Phrase
textarea(id="phrase")
button(id="phraseButton") Say It
.phrases
View Compiled
$font-text: "Roboto", "Lato", sans-serif
$background-color: #f0faff
$border-color: #a2a2a2
$error-color: lighten(red, 10%)
[hidden]
display: none
html
font-size: 62.5%
box-sizing: border-box
body
font-size: 18px
font-family: $font-text
line-height: 1.5em
color: #333
background-color: $background-color
*, *::before, *::after
box-sizing: inherit
.wrap
max-width: 800px
margin: 0 auto
button, input, textarea, select
font-size: 1.6rem
font-family: $font-text
display: inline-block
margin: .4rem 1rem
padding: .5rem 1rem
border-radius: 6px
background-color: white
color: #333
border: 1px solid $border-color
outline: none
text-align: left
textarea
display: block
width: 100%
min-height: 15vh
button
transition: all .33s linear
cursor: pointer
position: relative
&:hover
background-color: #333
color: white
border-color: #333
.close
text-align: center
&::before
content: 'x'
padding: 1px 0 0
display: inline-block
position: absolute
top: -0.8em
right: -0.8em
height: 1.4em
width: 1.4em
display: block
// padding: 4px
background-color: $error-color
color: white
font-size: 1.4rem
line-height: 1em
border-radius: 50%
cursor: pointer
&:hover
background-color: red
.box
display: inline-block
margin: 1rem 0
padding: .5rem 1rem
border-radius: 6px
border: 1px solid $border-color
background-color: darken($background-color, 10%)
&.block
display: block
width: 100%
&[aria-label="error"]
background-color: lighten($error-color, 20%)
&[aria-label="voices"]
background-color: darken($background-color, 4%)
&[aria-label="instructions"]
font-size: 1.4rem
&[aria-label="phrases"]
background-color: lighten(green, 67%)
View Compiled
var Talker = (function ($) {
var speech = window.speechSynthesis,
ret = {
nope: false
},
keyDown = {},
voices = [],
selectedVoice = null,
phrases = [
"Guten Morgen! Was gibt es zum Frühstück?",
"Okey dokey.",
"Hello world.",
"Cheeseburger in paradise.",
],
body,
app,
phrasesElement,
voicesElement,
selectedVoiceElement,
phraseTextbox,
phraseButton,
errorBlock
if (typeof speech === 'undefined') {
console.error("window.speechSynthesis doesn't exist. Please try a different browser that supports this feature.")
ret.nope = true
}
function populateVoiceList() {
var newVoices = speech.getVoices()
if (!selectedVoice && newVoices && newVoices.length > 0) {
voices = newVoices
selectedVoice = voices[0]
render()
}
}
function speak(phrase) {
if ( ! (phrase && /\w/i.test(phrase) && selectedVoice)) return
speech.cancel()
let utterance = new SpeechSynthesisUtterance(phrase)
utterance.voice = selectedVoice
speech.speak(utterance)
}
function render() {
voicesElement.html('')
phrasesElement.html('')
selectedVoiceElement.html('')
if (ret.nope) {
$(".box").hide()
errorBlock.show()
return
}
for (let i in phrases) {
let phrase = phrases[i]
let button = $("<button></button>")
button.data('phrase', phrase)
button.text(phrase)
button.append('<span class="close"/>')
phrasesElement.append(button)
}
for (let i in voices) {
let voice = voices[i]
if (!voice || !voice.name) {
continue
}
let voiceEl = $("<button />")
voiceEl.html(voice.name)
voiceEl.data('voice', voice)
if (selectedVoice && selectedVoice.name) {
}
voicesElement.append(voiceEl)
}
if (selectedVoice && selectedVoice.name) {
selectedVoiceElement.html(`Current voice: <strong>${selectedVoice.name}</strong>`)
}
}
function onPhraseClick(e) {
var me = $(e.currentTarget)
e.preventDefault()
var phrase = me.data('phrase')
if (phrase) {
phraseTextbox.val(phrase)
speak(phrase)
}
}
function onVoiceClick(e) {
var me = $(e.currentTarget)
e.preventDefault()
var voice = me.data('voice')
if (voice && voice.name) {
selectedVoice = voice
}
render()
if (keyDown['Shift']) {
let phrase = phraseTextbox.val()
if (phrase && typeof phrase === 'string') {
speak(phrase)
}
}
}
function onPhraseButtonClick(e) {
e.preventDefault()
processNewPhrase()
}
function onPhraseCloseClick(e) {
e.preventDefault()
e.stopPropagation()
var phrase = $(e.currentTarget).parent().data('phrase')
if (phrase && (keyDown['Shift'] || confirm('Really remove this phrase?'))) {
phrases = phrases.filter((el) => el !== phrase)
render()
}
}
function processNewPhrase() {
var newPhrase = phraseTextbox.val()
if (newPhrase && /\w/i.test(newPhrase)) {
if (!phrases.includes(newPhrase)) {
phrases.unshift(newPhrase)
}
render()
speak(newPhrase)
}
}
function onPhraseTextboxKeyPress(e) {
e.stopPropagation()
// console.log('onPhraseTextboxKeyPress', e)
switch (e.keyCode) {
case 13:
e.preventDefault()
processNewPhrase()
break;
}
}
function onAppKeyPress(e) {
// console.log(e)
switch (e.key) {
case 's':
case 'Enter':
let phrase = phraseTextbox.val()
if (phrase) {
speak(phrase)
}
break
case 'x':
speech.cancel()
break
}
}
function onAppKeyDown(e) {
// console.log(`key down: ${e.key}`)
keyDown[e.key] = true
}
function onAppKeyUp(e) {
keyDown[e.key] = false
}
function ready() {
app = $("#app")
body = $('body')
errorBlock = app.find('.block[aria-label="error"]')
phrasesElement = app.find('.phrases')
voicesElement = app.find('.voices')
selectedVoiceElement = app.find('.current-voice')
phraseTextbox = app.find('#phrase')
phraseButton = app.find('#phraseButton')
if (ret.nope) {
render()
return
}
body.on('keypress', onAppKeyPress)
body.on('keydown', onAppKeyDown)
body.on('keyup', onAppKeyUp)
app.on('click', '.phrases button', onPhraseClick)
app.on('click', '.phrases button .close', onPhraseCloseClick)
app.on('click', '.voices button', onVoiceClick)
phraseTextbox.on('keypress', onPhraseTextboxKeyPress)
phraseButton.on('click', onPhraseButtonClick)
populateVoiceList()
speech.onvoiceschanged = populateVoiceList
render()
}
$(ready)
return ret
})(jQuery)
View Compiled
This Pen doesn't use any external CSS resources.