javascript new function call with MooTools - mootools

I have found a MooTools version of Nivoo Slider that (in theory) will work with our MooTools dropdown menu. However, the menu is using MooTools 1.2.x, and Nivoo is using 1.3.2 or 1.4.0. Every time I try and use both the menu and the slider, the menu stops working.
Are the versions of the MooTools framework not backward compatible?
Also, are these plugins compatible or is one overriding the other?
I don't know enough about JS to correct my errors or rewrite the function call. Is there a good beginner's tutorial for this?
window.addEvent('domready', function () {
var menu = new UvumiDropdown('dropdown-demo');
// initialize Nivoo-Slider
new NivooSlider($('slider'), {
directionNavHide: true,
effect: 'wipeDown',
interval: 1000
});
});
In trying to convert without compatibility, here are instructions that I was not sure how to implement.
Instruction
:: Line of 1.2 code
$clear => use the native clearTimeout when using fn.delay, use clearInterval when using fn.periodical.
:: $clear(a.retrieve('closeDelay'))
myFn.create => Use the according functions like .pass, .bind, .delay, .periodical
:: this.createSubmenu(this.menu)
myFn.bind(this, [arg1, arg2, arg3]) => myFn.bind(this, arg1, arg2, arg3) OR myFn.pass([arg1, arg2, arg3], this)
:: this.domReady.bind(this)
$$ now only accepts a single selector, an array or arguments of elements
:: $$(b,b.getChildren('li')
These instructions are with compatibility. I'm trying both.
myElement.get('tween', options); // WRONG
myElement.set('tween', options).get('tween'); // YES, INDEED.
:: this.menu.get('tag')!='ul'
:: this.menu.getElement('ul')

OK I tested the UvumiDropdown latest build with mootools 1.4.x and it worked fine as long as I included a Mootools more build that includes Fx.Elements
Hope this helps
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/mootools/1.4.1/mootools-yui-compressed.js"> </script>
// MooTools: the javascript framework.
// Load this file's selection again by visiting: http://mootools.net/more/c8813373963b6a3e9a4d4bcfe9290081
// Or build this file again with packager using: packager build More/Fx.Elements
/*
---
script: More.js
name: More
description: MooTools More
license: MIT-style license
authors:
- Guillermo Rauch
- Thomas Aylott
- Scott Kyle
- Arian Stolwijk
- Tim Wienk
- Christoph Pojer
- Aaron Newton
- Jacob Thornton
requires:
- Core/MooTools
provides: [MooTools.More]
...
*/
MooTools.More = {
'version': '1.4.0.1',
'build': 'a4244edf2aa97ac8a196fc96082dd35af1abab87'
};
/*
---
script: Fx.Elements.js
name: Fx.Elements
description: Effect to change any number of CSS properties of any number of Elements.
license: MIT-style license
authors:
- Valerio Proietti
requires:
- Core/Fx.CSS
- /MooTools.More
provides: [Fx.Elements]
...
*/
Fx.Elements = new Class({
Extends: Fx.CSS,
initialize: function(elements, options){
this.elements = this.subject = $$(elements);
this.parent(options);
},
compute: function(from, to, delta){
var now = {};
for (var i in from){
var iFrom = from[i], iTo = to[i], iNow = now[i] = {};
for (var p in iFrom) iNow[p] = this.parent(iFrom[p], iTo[p], delta);
}
return now;
},
set: function(now){
for (var i in now){
if (!this.elements[i]) continue;
var iNow = now[i];
for (var p in iNow) this.render(this.elements[i], p, iNow[p], this.options.unit);
}
return this;
},
start: function(obj){
if (!this.check(obj)) return this;
var from = {}, to = {};
for (var i in obj){
if (!this.elements[i]) continue;
var iProps = obj[i], iFrom = from[i] = {}, iTo = to[i] = {};
for (var p in iProps){
var parsed = this.prepare(this.elements[i], p, iProps[p]);
iFrom[p] = parsed.from;
iTo[p] = parsed.to;
}
}
return this.parent(from, to);
}
});
/*
UvumiTools Dropdown Menu v1.1.2 http://uvumi.com/tools/dropdown.html
Copyright (c) 2009 Uvumi LLC
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
var UvumiDropdown = new Class({
Implements:Options,
options:{
clickToOpen:false, //if set to true, must click to open submenues
openDelay:150, //if hover mode, duration the mouse must stay on target before submenu is opened. if exits before delay expires, timer is cleared
closeDelay:500, //delay before the submenu close when mouse exits. If mouse enter the submenu again before timer expires, it's cleared
duration:250, //duration in millisecond of opening/closing effect
link:'cancel',
transition:Fx.Transitions.linear, //effect's transitions. See http://docs.mootools.net/Fx/Fx.Transitions for more details
mode:'horizontal' //if set to horizontal, the top level menu will be displayed horizontally. If set to vertical, it will be displayed vertically. If it does not match any of those two words, 'horizontal' will be used.
},
initialize: function(menu,options){
this.menu = menu;
this.setOptions(options);
if(this.options.mode != 'horizontal' && this.options.mode != 'vertical'){
this.options.mode = 'horizontal';
}
//some versions of Safari and Chrome run domready before DOM is actually ready, causing wrong positioning. If you still have some display issues in those browser try to increase the delay value a bit. I tried to keep it as low as possible, but sometimes it can take a bit longer than expected
if(Browser.Engine.webkit){
window.addEvent('domready',this.domReady.delay(200,this));
}else{
window.addEvent('domready',this.domReady.bind(this));
}
},
domReady:function(){
this.menu = $(this.menu);
if(!$defined(this.menu)){
return false;
}
//if passed element is not a UL, tries to find one in the children elements
if(this.menu.get('tag')!='ul'){
this.menu = this.menu.getElement('ul');
if(!$defined(this.menu)){
return false;
}
}
//handles pages written form right to left.
if(this.menu.getStyle('direction') == 'rtl' || $(document.body).getStyle('direction') == 'rtl'){
this.rtl = true;
if(Browser.Engine.trident && $(document.body).getStyle('direction') == 'rtl'){
this.menu.getParent().setStyle('direction','ltr');
this.menu.setStyle('direction','rtl');
}
}
//start setup
this.menu.setStyles({
visibility:'hidden',
display:'block',
overflow:'hidden',
height:0,
marginLeft:(Browser.Engine.trident?1:-1)
});
//we call the createSubmenu function on the main UL, which is a recursive function
this.createSubmenu(this.menu);
//the LIs must be floated to be displayed horisotally
if(this.options.mode=='horizontal'){
this.menu.getChildren('li').setStyles({
'float':(this.rtl?'right':'left'),
display:'block',
top:0
});
//We create an extar LI which role will be to clear the floats of the others
var clear = new Element('li',{
html:" ",
styles:{
clear:(this.rtl?'right':'left'),
display:(Browser.Engine.trident?'inline':'block'), //took me forever to find that fix
position:'relative',
top:0,
height:0,
width:0,
fontSize:0,
lineHeight:0,
margin:0,
padding:0
}
}).inject(this.menu);
}else{
this.menu.getChildren('li').setStyles({
display:'block',
top:0
});
}
this.menu.setStyles({
height:'auto',
overflow:'visible',
visibility:'visible'
});
//hack for IE, again
this.menu.getElements('a').setStyle('display',(Browser.Engine.trident?'inline-block':'block'));
},
createSubmenu:function(ul){
//we collect all the LI of the ul
var LIs = ul.getChildren('li');
var offset = 0;
//loop through the LIs
LIs.each(function(li){
li.setStyles({
position:'relative',
display:'block',
top:-offset,
zIndex:1
});
offset += li.getSize().y;
var innerUl = li.getFirst('ul');
//if the current LI contains a UL
if($defined(innerUl)){
ul.getElements('ul').setStyle('display','none');
//if the current UL is the main one, that means we are still in the top row, and the submenu must drop down
if(ul == this.menu && this.options.mode == 'horizontal'){
li.addClass('submenu-down');
var x = 0;
var y = li.getSize().y;
this.options.link='cancel';
li.store('animation',new Fx.Elements($$(innerUl,innerUl.getChildren('li')).setStyle('opacity',0),this.options));
//if the current UL is not the main one, the sub menu must pop from the side
}else{
li.addClass('submenu-left');
var x = li.getSize().x-(this.rtl&&!Browser.Engine.trident?2:1)*li.getStyle('border-left-width').toInt();
var y = -li.getStyle('border-bottom-width').toInt();
this.options.link='chain';
li.store('animation',new Fx.Elements($$(innerUl,innerUl.getChildren('li')).setStyle('opacity',0),this.options));
offset=li.getSize().y+li.getPosition(this.menu).y;
}
innerUl.setStyles({
position:'absolute',
top:y,
opacity:0
});
ul.getElements('ul').setStyle('display','block');
if(this.rtl){
innerUl.setStyles({
right:x,
marginRight:-x
});
}else{
innerUl.setStyles({
left:x,
marginLeft:-x
});
}
//we call the createsubmenu function again, on the new UL
this.createSubmenu(innerUl);
//apply events to make the submenu appears when hovering the LI
if(this.options.clickToOpen){
li.addEvent('mouseenter',function(){
$clear(li.retrieve('closeDelay'));
}.bind(this)
);
li.getFirst('a').addEvent('click',function(e){
e.stop();
$clear(li.retrieve('closeDelay'));
this.showChildList(li);
}.bind(this));
}else{
li.addEvent('mouseenter',function(){
$clear(li.retrieve('closeDelay'));
li.store('openDelay',this.showChildList.delay(this.options.openDelay,this,li));
}.bind(this));
}
li.addEvent('mouseleave', function(){
$clear(li.retrieve('openDelay'));
li.store('closeDelay',this.hideChildList.delay(this.options.closeDelay,this,li));
}.bind(this));
}
},this);
},
//display submenu
showChildList:function(li){
var ul = li.getFirst('ul');
var LIs = $$(ul.getChildren('li'));
var animation = li.retrieve('animation');
//if the parent menu is not the main menu, the submenu must pop from the side
if(li.getParent('ul')!=this.menu || this.options.mode == 'vertical'){
animation.cancel();
var anim ={
0:{
opacity:1
},
1:{
opacity:1
}
};
if(this.rtl){
anim[0]['marginRight'] = 0;
}else{
anim[0]['marginLeft'] = 0;
}
animation.start(anim);
var animobject={};
//if the parent menu us the main menu, the submenu must drop down
}else{
var animobject = {0:{opacity:1}};
}
LIs.each(function(innerli,i){
animobject[i+1]={
top:0,
opacity:1
};
});
li.setStyle('z-index',99);
animation.start(animobject);
},
//hide the menu
hideChildList:function(li){
var animation = li.retrieve('animation');
var ul = li.getFirst('ul');
var LIs = $$(ul.getChildren('li'));
var offset = 0;
var animobject={};
LIs.each(function(innerli,i){
animobject[i+1]={
top:-offset,
opacity:0
};
offset += innerli.getSize().y;
});
li.setStyle('z-index',1);
//if the parent menu is not the main menu, the submenu must fold up, and go to the left
if(li.getParent('ul')!=this.menu || this.options.mode == 'vertical'){
animobject[1]=null;
animation.cancel();
animation.start(animobject);
var anim = {
0:{
opacity:0
},
1:{
opacity:0
}
};
if(this.rtl){
anim[0]['marginRight'] = -ul.getSize().x;
}else{
anim[0]['marginLeft'] = -ul.getSize().x;
}
animation.start(anim);
//if the parent menu is the main menu, the submenu must just fold up
}else{
animobject[0]={opacity:0};
animation.start(animobject);
}
}
});

MooTools follows SemVer (Semantic Versioning), meaning that a minor version number (x.Y.z) bump is not guaranteed to be backward-compatible (and is usually not).
However, new versions come with a compatibility layer. Just tick the box on the MooTools Core builder if you really can't upgrade your code. You should though avoid to do such a thing, it is bad for performance and potentially forward-compatibility.
As for a tutorial, the best way to learn how to upgrade code from one version to the other is to read the changelog of the 1.3 to learn about the differences with 1.2, and from the 1.3 to the 1.4 if you want to upgrade to the latest version. From this knowledge, rewrite all calls that make use of outdated APIs.
It looks like a daunting task at first, but it usually goes very quickly (actually, in this precise case, it is most often about rewriting Hash references and .each calls). It might be hard if you're learning JS, but it will definitely be a very rewarding experience in JS, and especially in MooTools, as you'll learn about what makes a code ”Mooish” :)

Related

TVML listItemLockup click event

I'm using the 'Compilation.xml' template from the TVMLCatalog
I'd like to add a button click event to a 'listItemLockup'
<listItemLockup>
<ordinal minLength="2" class="ordinalLayout">0</ordinal>
<title>Intro</title>
<subtitle>00</subtitle>
<decorationLabel>(3:42)</decorationLabel>
</listItemLockup>
I've tried adding:
App.onLaunch = function(options) {
var templateURL = 'http://localhost:8000/hello.tvml';
var doc = getDocument(templateURL);
//doc.addEventListener("select", function() { alert("CLICK!") }, false);
var listItemLockupElement = doc.getElementsByTagName("listItemLockup");
listItemLockupElement.addEventListener("select", function() { alert("CLICK!") }, false);
}
addEventListener
void addEventListener (in String type, in Object listener, in optional Object extraInfo)
Is "select" the correct type?
I've been using the following tutorials
http://jamesonquave.com/blog/developing-tvos-apps-for-apple-tv-with-swift/
http://jamesonquave.com/blog/developing-tvos-apps-for-apple-tv-part-2/
Update
I'm getting an error
ITML <Error>: doc.getElementsByTagName is not a function. (In 'doc.getElementsByTagName("listItemLockup")', 'doc.getElementsByTagName' is undefined) - http://localhost:8000/main.js - line:27:58
I tried adding this to the 'onLaunch'
var listItemLockupElements = doc.getElementsByTagName("listItemLockup");
for (var i = 0; i < listItemLockupElements.length; i++) {
//var ele = listItemLockupElements[i].firstChild.nodeValue;
listItemLockupElements[i].addEventListener("select", function() { alert("CLICK!") }, false);
}
I'll see about the error first
Cross Post: https://forums.developer.apple.com/thread/17859
More common example I have seen by Apple is to define a single overall listener like:
doc.addEventListener("select", Presenter.load.bind(Presenter));
In your xml, assign unique ids to elements, or give them ways to identify them.
For example, the beginning would be something like:
load: function(event) {
var self = this,
ele = event.target,
attr_id = ele.getAttribute("id"),
audioURL = ele.getAttribute("audioURL"),
videoURL = ele.getAttribute("videoURL")
And then you can do whatever you want with your item.
if(audioURL && (event.type === "select" || event.type === "play")) {
//
}
My advice would be to study the Presenter.js file more carefully for this pattern.
Edit:
Answering your "Update" related to doc.getElementsByTagName is not a function. "doc" does not actually exist, but the general pattern is to get it with
var doc = getActiveDocument();
I assumed you would know the above.
Does that fix it?
var listItemLockupElement = doc.getElementsByTagName("listItemLockup”);
In this case, the listItemLockupElement is a NodeList, not an element. You can either iterate through the list and add an event listener to each listItemLockup, or you could add the event listener to the containing element.
When addressing items in a NodeList, you use the item(i) method rather than the standard array access notation:
listItemLockupElements.item(i).addEventListener("select", function() { })
See: https://developer.mozilla.org/en-US/docs/Web/API/NodeList/item
Adding event listeners is straightforward if you're using atvjs framework.
ATV.Page.create({
name: 'mypage',
template: your_template_function,
data: your_data,
events: {
select: 'onSelect',
},
// method invoked in the scope of the current object and
// 'this' will be bound to the object at runtime
// so you can easily access methods and properties and even modify them at runtime
onSelect: function(e) {
let element = e.target;
let elementType = element.nodeName.toLowerCase();
if (elementType === 'listitemlockup') {
this.doSomething();
}
},
doSomething: function() {
// some awesome action
}
});
ATV.Navigation.navigate('mypage');
Disclaimer: I am the creator and maintainer of atvjs and as of writing this answer, it is the only JavaScript framework available for Apple TV development using TVML and TVJS. Hence I could provide references only from this framework. The answer should not be mistaken as a biased opinion.

Getting the list of voices in speechSynthesis (Web Speech API)

Following HTML shows empty array in console on first click:
<!DOCTYPE html>
<html>
<head>
<script>
function test(){
console.log(window.speechSynthesis.getVoices())
}
</script>
</head>
<body>
Test
</body>
</html>
In second click you will get the expected list.
If you add onload event to call this function (<body onload="test()">), then you can get correct result on first click. Note that the first call on onload still doesn't work properly. It returns empty on page load but works afterward.
Questions:
Since it might be a bug in beta version, I gave up on "Why" questions.
Now, the question is if you want to access window.speechSynthesis on page load:
What is the best hack for this issue?
How can you make sure it will load speechSynthesis, on page load?
Background and tests:
I was testing the new features in Web Speech API, then I got to this problem in my code:
<script type="text/javascript">
$(document).ready(function(){
// Browser support messages. (You might need Chrome 33.0 Beta)
if (!('speechSynthesis' in window)) {
alert("You don't have speechSynthesis");
}
var voices = window.speechSynthesis.getVoices();
console.log(voices) // []
$("#test").on('click', function(){
var voices = window.speechSynthesis.getVoices();
console.log(voices); // [SpeechSynthesisVoice, ...]
});
});
</script>
<a id="test" href="#">click here if 'ready()' didn't work</a>
My question was: why does window.speechSynthesis.getVoices() return empty array, after page is loaded and onready function is triggered? As you can see if you click on the link, same function returns an array of available voices of Chrome by onclick triger?
It seems Chrome loads window.speechSynthesis after the page load!
The problem is not in ready event. If I remove the line var voice=... from ready function, for first click it shows empty list in console. But the second click works fine.
It seems window.speechSynthesis needs more time to load after first call. You need to call it twice! But also, you need to wait and let it load before second call on window.speechSynthesis. For example, following code shows two empty arrays in console if you run it for first time:
// First speechSynthesis call
var voices = window.speechSynthesis.getVoices();
console.log(voices);
// Second speechSynthesis call
voices = window.speechSynthesis.getVoices();
console.log(voices);
According to Web Speech API Errata (E11 2013-10-17), the voice list is loaded async to the page. An onvoiceschanged event is fired when they are loaded.
voiceschanged: Fired when the contents of the SpeechSynthesisVoiceList, that the getVoices method will return, have changed. Examples include: server-side synthesis where the list is determined asynchronously, or when client-side voices are installed/uninstalled.
So, the trick is to set your voice from the callback for that event listener:
// wait on voices to be loaded before fetching list
window.speechSynthesis.onvoiceschanged = function() {
window.speechSynthesis.getVoices();
...
};
You can use a setInterval to wait until the voices are loaded before using them however you need and then clearing the setInterval:
var timer = setInterval(function() {
var voices = speechSynthesis.getVoices();
console.log(voices);
if (voices.length !== 0) {
var msg = new SpeechSynthesisUtterance(/*some string here*/);
msg.voice = voices[/*some number here to choose from array*/];
speechSynthesis.speak(msg);
clearInterval(timer);
}
}, 200);
$("#test").on('click', timer);
After studying the behavior on Google Chrome and Firefox, this is what can get all voices:
Since it involves something asynchronous, it might be best done with a promise:
const allVoicesObtained = new Promise(function(resolve, reject) {
let voices = window.speechSynthesis.getVoices();
if (voices.length !== 0) {
resolve(voices);
} else {
window.speechSynthesis.addEventListener("voiceschanged", function() {
voices = window.speechSynthesis.getVoices();
resolve(voices);
});
}
});
allVoicesObtained.then(voices => console.log("All voices:", voices));
Note:
When the event voiceschanged fires, we need to call .getVoices() again. The original array won't be populated with content.
On Google Chrome, we don't have to call getVoices() initially. We only need to listen on the event, and it will then happen. On Firefox, listening is not enough, you have to call getVoices() and then listen on the event voiceschanged, and set the array using getVoices() once you get notified.
Using a promise makes the code more clean. Everything related to getting voices are in this promise code. If you don't use a promise but instead put this code in your speech routine, it is quite messy.
You can write a voiceObtained promise to resolve to a voice you want, and then your function to say something can just do: voiceObtained.then(voice => { }) and inside that handler, call the window.speechSynthesis.speak() to speak something. Or you can even write a promise speechReady("hello world").then(speech => { window.speechSynthesis.speak(speech) }) to say something.
heres the answer
function synthVoice(text) {
const awaitVoices = new Promise(resolve=>
window.speechSynthesis.onvoiceschanged = resolve)
.then(()=> {
const synth = window.speechSynthesis;
var voices = synth.getVoices();
console.log(voices)
const utterance = new SpeechSynthesisUtterance();
utterance.voice = voices[3];
utterance.text = text;
synth.speak(utterance);
});
}
At first i used onvoiceschanged , but it kept firing even after the voices was loaded, so my goal was to avoid onvoiceschanged at all cost.
This is what i came up with. It seems to work so far, will update if it breaks.
loadVoicesWhenAvailable();
function loadVoicesWhenAvailable() {
voices = synth.getVoices();
if (voices.length !== 0) {
console.log("start loading voices");
LoadVoices();
}
else {
setTimeout(function () { loadVoicesWhenAvailable(); }, 10)
}
}
setInterval solution by Salman Oskooi was perfect
Please see https://jsfiddle.net/exrx8e1y/
function myFunction() {
dtlarea=document.getElementById("details");
//dtlarea.style.display="none";
dtltxt="";
var mytimer = setInterval(function() {
var voices = speechSynthesis.getVoices();
//console.log(voices);
if (voices.length !== 0) {
var msg = new SpeechSynthesisUtterance();
msg.rate = document.getElementById("rate").value; // 0.1 to 10
msg.pitch = document.getElementById("pitch").value; //0 to 2
msg.volume = document.getElementById("volume").value; // 0 to 1
msg.text = document.getElementById("sampletext").value;
msg.lang = document.getElementById("lang").value; //'hi-IN';
for(var i=0;i<voices.length;i++){
dtltxt+=voices[i].lang+' '+voices[i].name+'\n';
if(voices[i].lang==msg.lang) {
msg.voice = voices[i]; // Note: some voices don't support altering params
msg.voiceURI = voices[i].voiceURI;
// break;
}
}
msg.onend = function(e) {
console.log('Finished in ' + event.elapsedTime + ' seconds.');
dtlarea.value=dtltxt;
};
speechSynthesis.speak(msg);
clearInterval(mytimer);
}
}, 1000);
}
This works fine on Chrome for MAC, Linux(Ubuntu), Windows and Android
Android has non-standard en_GB wile others have en-GB as language code
Also you will see that same language(lang) has multiple names
On Mac Chrome you get en-GB Daniel besides en-GB Google UK English Female and n-GB Google UK English Male
en-GB Daniel (Mac and iOS)
en-GB Google UK English Female
en-GB Google UK English Male
en_GB English United Kingdom
hi-IN Google हिन्दी
hi-IN Lekha (Mac and iOS)
hi_IN Hindi India
Another way to ensure voices are loaded before you need them is to bind their loading state to a promise, and then dispatch your speech commands from a then:
const awaitVoices = new Promise(done => speechSynthesis.onvoiceschanged = done);
function listVoices() {
awaitVoices.then(()=> {
let voices = speechSynthesis.getVoices();
console.log(voices);
});
}
When you call listVoices, it will either wait for the voices to load first, or dispatch your operation on the next tick.
I used this code to load voices successfully:
<select id="voices"></select>
...
function loadVoices() {
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
}
function populateVoiceList() {
var allVoices = speechSynthesis.getVoices();
allVoices.forEach(function(voice, index) {
var option = $('<option>').val(index).html(voice.name).prop("selected", voice.default);
$('#voices').append(option);
});
if (allVoices.length > 0 && speechSynthesis.onvoiceschanged !== undefined) {
// unregister event listener (it is fired multiple times)
speechSynthesis.onvoiceschanged = null;
}
}
I found the 'onvoiceschanged' code from this article: https://hacks.mozilla.org/2016/01/firefox-and-the-web-speech-api/
Note: requires JQuery.
Works in Firefox/Safari and Chrome (and in Google Apps Script too - but only in the HTML).
async function speak(txt) {
await initVoices();
const u = new SpeechSynthesisUtterance(txt);
u.voice = speechSynthesis.getVoices()[3];
speechSynthesis.speak(u);
}
function initVoices() {
return new Promise(function (res, rej){
speechSynthesis.getVoices();
if (window.speechSynthesis.onvoiceschanged) {
res();
} else {
window.speechSynthesis.onvoiceschanged = () => res();
}
});
}
While the accepted answer works great but if you're using SPA and not loading full-page, on navigating between links, the voices will not be available.
This will run on a full-page load
window.speechSynthesis.onvoiceschanged
For SPA, it wouldn't run.
You can check if it's undefined, run it, or else, get it from the window object.
An example that works:
let voices = [];
if(window.speechSynthesis.onvoiceschanged == undefined){
window.speechSynthesis.onvoiceschanged = () => {
voices = window.speechSynthesis.getVoices();
}
}else{
voices = window.speechSynthesis.getVoices();
}
// console.log("voices", voices);
I had to do my own research for this to make sure I understood it properly, so just sharing (feel free to edit).
My goal is to:
Get a list of voices available on my device
Populate a select element with those voices (after a particular page loads)
Use easy to understand code
The basic functionality is demonstrated in MDN's official live demo of:
https://github.com/mdn/web-speech-api/tree/master/speak-easy-synthesis
but I wanted to understand it better.
To break the topic down...
SpeechSynthesis
The SpeechSynthesis interface of the Web Speech API is the controller
interface for the speech service; this can be used to retrieve
information about the synthesis voices available on the device, start
and pause speech, and other commands besides.
Source
onvoiceschanged
The onvoiceschanged property of the SpeechSynthesis interface
represents an event handler that will run when the list of
SpeechSynthesisVoice objects that would be returned by the
SpeechSynthesis.getVoices() method has changed (when the voiceschanged
event fires.)
Source
Example A
If my application merely has:
var synth = window.speechSynthesis;
console.log(synth);
console.log(synth.onvoiceschanged);
Chrome developer tools console will show:
Example B
If I change the code to:
var synth = window.speechSynthesis;
console.log("BEFORE");
console.log(synth);
console.log(synth.onvoiceschanged);
console.log("AFTER");
var voices = synth.getVoices();
console.log(voices);
console.log(synth);
console.log(synth.onvoiceschanged);
The before and after states are the same, and voices is an empty array.
Solution
Although i'm not confident implementing Promises, the following worked for me:
Defining the function
var synth = window.speechSynthesis;
// declare so that values are accessible globally
var voices = [];
function set_up_speech() {
return new Promise(function(resolve, reject) {
// get the voices
var voices = synth.getVoices();
// get reference to select element
var $select_topic_speaking_voice = $("#select_topic_speaking_voice");
// for each voice, generate select option html and append to select
for (var i = 0; i < voices.length; i++) {
var option = $("<option></option>");
var suffix = "";
// if it is the default voice, add suffix text
if (voices[i].default) {
suffix = " -- DEFAULT";
}
// create the option text
var option_text = voices[i].name + " (" + voices[i].lang + suffix + ")";
// add the option text
option.text(option_text);
// add option attributes
option.attr("data-lang", voices[i].lang);
option.attr("data-name", voices[i].name);
// append option to select element
$select_topic_speaking_voice.append(option);
}
// resolve the voices value
resolve(voices)
});
}
Calling the function
// in your handler, populate the select element
if (page_title === "something") {
set_up_speech()
}
Android Chrome - turn off data saver. It was helpfull for me.(Chrome 71.0.3578.99)
// wait until the voices load
window.speechSynthesis.onvoiceschanged = function() {
window.speechSynthesis.getVoices();
};
let voices = speechSynthesis.getVoices();
let gotVoices = false;
if (voices.length) {
resolve(voices, message);
} else {
speechSynthesis.onvoiceschanged = () => {
if (!gotVoices) {
voices = speechSynthesis.getVoices();
gotVoices = true;
if (voices.length) resolve(voices, message);
}
};
}
function resolve(voices, message) {
var synth = window.speechSynthesis;
let utter = new SpeechSynthesisUtterance();
utter.lang = 'en-US';
utter.voice = voices[65];
utter.text = message;
utter.volume = 100.0;
synth.speak(utter);
}
Works for Edge, Chrome and Safari - doesn't repeat the sentences.

Repeatedly Grab DOM in Chrome Extension

I'm trying to teach myself how to write Chrome extensions and ran into a snag when I realized that my jQuery was breaking because it was getting information from the extension page itself and not the tab's current page like I had expected.
Quick summary, my sample extension will refresh the page every x seconds, look at the contents/DOM, and then do some stuff with it. The first and last parts are fine, but getting the DOM from the page that I'm on has proven very difficult, and the documentation hasn't been terribly helpful for me.
You can see the code that I have so far at these links:
Current manifest
Current js script
Current popup.html
If I want to have the ability to grab the DOM on each cycle of my setInterval call, what more needs to be done? I know that, for example, I'll need to have a content script. But do I also need to specify a background page in my manifest? Where do I need to call the content script within my extension? What's the easiest/best way to have it communicate with my current js file on each reload? Will my content script also be expecting me to use jQuery?
I know that these questions are basic and will seem trivial to me in retrospect, but they've really been a headache trying to explore completely on my own. Thanks in advance.
In order to access the web-pages DOM you'll need to programmatically inject some code into it (using chrome.tabs.executeScript()).
That said, although it is possible to grab the DOM as a string, pass it back to your popup, load it into a new element and look for what ever you want, this is a really bad approach (for various reasons).
The best option (in terms of efficiency and accuracy) is to do the processing in web-page itself and then pass just the results back to the popup. Note that in order to be able to inject code into a web-page, you have to include the corresponding host match pattern in your permissions property in manifest.
What I describe above can be achieved like this:
editorMarket.js
var refresherID = 0;
var currentID = 0;
$(document).ready(function(){
$('.start-button').click(function(){
oldGroupedHTML = null;
oldIndividualHTML = null;
chrome.tabs.query({ active: true }, function(tabs) {
if (tabs.length === 0) {
return;
}
currentID = tabs[0].id;
refresherID = setInterval(function() {
chrome.tabs.reload(currentID, { bypassCache: true }, function() {
chrome.tabs.executeScript(currentID, {
file: 'content.js',
runAt: 'document_idle',
allFrames: false
}, function(results) {
if (chrome.runtime.lastError) {
alert('ERROR:\n' + chrome.runtime.lastError.message);
return;
} else if (results.length === 0) {
alert('ERROR: No results !');
return;
}
var nIndyJobs = results[0].nIndyJobs;
var nGroupJobs = results[0].nGroupJobs;
$('.lt').text('Indy: ' + nIndyJobs + '; '
+ 'Grouped: ' + nGroupJobs);
});
});
}, 5000);
});
});
$('.stop-button').click(function(){
clearInterval(refresherID);
});
});
content.js:
(function() {
function getNumberOfIndividualJobs() {...}
function getNumberOfGroupedJobs() {...}
function comparator(grouped, individual) {
var IndyJobs = getNumberOfIndividualJobs();
var GroupJobs = getNumberOfGroupedJobs();
nIndyJobs = IndyJobs[1];
nGroupJobs = GroupJobs[1];
console.log(GroupJobs);
return {
nIndyJobs: nIndyJobs,
nGroupJobs: nGroupJobs
};
}
var currentGroupedHTML = $(".grouped_jobs").html();
var currentIndividualHTML = $(".individual_jobs").html();
var result = comparator(currentGroupedHTML, currentIndividualHTML);
return result;
})();

Mootools reuse same function on multiple instances with something like the each function

I'm using the mootools wall plugin, Its working well in my application, however if I add multiple (image) walls it only works for one wall ::: My understanding of scripting is not good enough to add a each function or similar :::
I need to "bind" the code below to say 2 divs like this :::
My First wall:
<div id="viewport">
<div id="wall">
Second wall:
<div id="viewport">
<div id="wall_02">
Any assistance would be appreciated.
var wallIMAGES = new Wall( "wall", {
"width": scArray[1],
"height": scArray[1],
callOnUpdate: function(items){
items.each(function(e, i){
var el = wall[counterFluid];
if (el) {
var a = new Element("img[width="+scArray[1]+"][height="+scArray[1]+"][src={thumb}]".substitute(el));
a.inject(e.node).set("opacity", 0).fade("in");
e.node.store("tubeObject", el);
}
counterFluid++;
// Reset counter
if( counterFluid >= scArray[10].length) counterFluid = 0;
})
}
});
wallIMAGES.initWall();
Maybe something like this:
var my_wall_ids = ['wall', 'wall_02'];
var myWalls = [];
var baseWallOptions = {
"width": scArray[1],
"height": scArray[1],
callOnUpdate: function(items){
items.each(function(e, i){
var el = wall[counterFluid];
if (el) {
var a = new Element("img[width="+scArray[1]+"][height="+scArray[1]+"][src={thumb}]".substitute(el));
a.inject(e.node).set("opacity", 0).fade("in");
e.node.store("tubeObject", el);
}
counterFluid++;
// Reset counter
if( counterFluid >= scArray[10].length) {counterFluid = 0;}
}); // end items.each
}
}
for (var i=0;i<my_wall_ids.length;i++){
var id = my_wall_ids[i];
var wallOptions = baseWallOptions;
// if your customization was something like changing
// the height , but only on the 'wall' element
if (id === 'wall') {
wallOptions.height = 400;
}
myWalls[i] = new Wall(id, wallOptions);
myWalls[i].initWall();
}
If you read Wall's documentation, you'll notice that, just like most other classes, the first argument it takes is an element id.
So, if your initialization code states
new Wall("wall", { …
…then it will be applied to the element that has the id "wall".
You could simply duplicate your code and use one with "wall", the other one with "wall_02". However, that would be bad practice. Indeed, if you later wanted to change some options, you'd have to do it in two distinct blocks, and they would probably get out of sync.
If your only difference lies in the target id, and the options are to be shared, simply store the options object (second parameter to the Wall class) in a variable and use it twice! That is:
var wallOptions = { width: … };
var wallImages = new Wall("wall", wallOptions),
wallImages2 = new Wall("wall_02", wallOptions);
wallImages.initWall();
wallImages2.initWall();
It could be even better to embed initalization in a function, but this solution is probably the easiest if you simply want to have two Wall instances without learning much more about JS.

How do I replace Function create from MooTools 1.2 to 1.3?

Hi there i have this code snippet I need to get working with MooTools 1.3 :
this.fn = function (e, cal) {
var e = new Event(e);
var el = e.target;
var stop = false;
while (el != document.body && el.nodeType == 1) {
if (el == this.calendar) { stop = true; }
this.calendars.each(function (kal) {
if (kal.button == el || kal.els.contains(el)) { stop = true; }
});
if (stop) {
e.stop();
return false;
}
else { el = el.parentNode; }
}
this.toggle(cal);
}.create({
'arguments': cal,
'bind': this,
'event': true
}); <-- THIS CREATE METHOD DOES NOT WORK
Can someone help me whit this ?
After create function was deprecated, you need to "manually" recreate the usage.
In this case, you are creating a function that will be an event listener and binding it later in the code (this is Aeron Glemann's calendar).
So what you need to do, is put this function in addEvent you find directly below it, like this.
document.addEvent('mousedown', function(e, cal) {
[...]
}.bind(this));
Also, there's a removeEvent call at the begining of the function you're currently editing (the toggle function) that will no longer work since this function no longer has a name, replace it with removing all events on mousedown, worked for me.
document.removeEvents('mousedown');
as i said on the mootools user mailing list, i don't know about the "perfect" way, but in the meantime you can always (if you don't want to use the 1.2 compat version)
inspire yourself from the implementation of the function for 1.2 compat :
https://github.com/mootools/mootools-core/blob/025adc07dc7e9851f30b3911961d43d525d83847/Source/Types/Function.js#L74
I have to admit the doc for 1.3 only mention that this method is deprecated.