Html5 & JavaScript Text to Speech conversion [duplicate] - javascript

I am getting a problem when trying to use Speech Synthesis API in Chrome 33. It works perfectly with a shorter text, but if I try longer text, it just stops in the middle. After it has stopped once like that, the Speech Synthesis does not work anywhere within Chrome until the browser is restarted.
Example code (http://jsfiddle.net/Mdm47/1/):
function speak(text) {
var msg = new SpeechSynthesisUtterance();
var voices = speechSynthesis.getVoices();
msg.voice = voices[10];
msg.voiceURI = 'native';
msg.volume = 1;
msg.rate = 1;
msg.pitch = 2;
msg.text = text;
msg.lang = 'en-US';
speechSynthesis.speak(msg);
}
speak('Short text');
speak('Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.');
speak('Another short text');
It stops speaking in the middle of the second text, and I can't get any other page to speak after that.
Is it a browser bug or some kind of security limitation?

I've had this issue for a while now with Google Chrome Speech Synthesis. After some investigation, I discovered the following:
The breaking of the utterances only happens when the voice is not a native voice,
The cutting out usually occurs between 200-300 characters,
When it does break you can un-freeze it by doing speechSynthesis.cancel();
The 'onend' event sometimes decides not to fire. A quirky work-around to this is to console.log() out the utterance object before speaking it. Also I found wrapping the speak invocation in a setTimeout callback helps smooth these issues out.
In response to these problems, I have written a function that overcomes the character limit, by chunking the text up into smaller utterances, and playing them one after another. Obviously you'll get some odd sounds sometimes as sentences might be chunked into two separate utterances with a small time delay in between each, however the code will try and split these points at punctuation marks as to make the breaks in sound less obvious.
Update
I've made this work-around publicly available at https://gist.github.com/woollsta/2d146f13878a301b36d7#file-chunkify-js. Many thanks to Brett Zamir for his contributions.
The function:
var speechUtteranceChunker = function (utt, settings, callback) {
settings = settings || {};
var newUtt;
var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
newUtt = utt;
newUtt.text = txt;
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
}
if (callback !== undefined) {
callback();
}
});
}
else {
var chunkLength = (settings && settings.chunkLength) || 160;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
var chunkArr = txt.match(pattRegex);
if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
//call once all text has been spoken...
if (callback !== undefined) {
callback();
}
return;
}
var chunk = chunkArr[0];
newUtt = new SpeechSynthesisUtterance(chunk);
var x;
for (x in utt) {
if (utt.hasOwnProperty(x) && x !== 'text') {
newUtt[x] = utt[x];
}
}
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
return;
}
settings.offset = settings.offset || 0;
settings.offset += chunk.length - 1;
speechUtteranceChunker(utt, settings, callback);
});
}
if (settings.modifier) {
settings.modifier(newUtt);
}
console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
}, 0);
};
How to use it...
//create an utterance as you normally would...
var myLongText = "This is some long text, oh my goodness look how long I'm getting, wooooohooo!";
var utterance = new SpeechSynthesisUtterance(myLongText);
//modify it as you normally would
var voiceArr = speechSynthesis.getVoices();
utterance.voice = voiceArr[2];
//pass it into the chunking function to have it played out.
//you can set the max number of characters by changing the chunkLength property below.
//a callback function can also be added that will fire once the entire text has been spoken.
speechUtteranceChunker(utterance, {
chunkLength: 120
}, function () {
//some code to execute when done
console.log('done');
});
Hope people find this as useful.

I have solved the probleme while having a timer function which call the pause() and resume() function and callset the timer again. On the onend event I clear the timer.
var myTimeout;
function myTimer() {
window.speechSynthesis.pause();
window.speechSynthesis.resume();
myTimeout = setTimeout(myTimer, 10000);
}
...
window.speechSynthesis.cancel();
myTimeout = setTimeout(myTimer, 10000);
var toSpeak = "some text";
var utt = new SpeechSynthesisUtterance(toSpeak);
...
utt.onend = function() { clearTimeout(myTimeout); }
window.speechSynthesis.speak(utt);
...
This seem to work well.

A simple and effective solution is to resume periodically.
function resumeInfinity() {
window.speechSynthesis.resume();
timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
}
You can associate this with the onend and onstart events, so you will only be invoking the resume if necessary. Something like:
var utterance = new SpeechSynthesisUtterance();
utterance.onstart = function(event) {
resumeInfinity();
};
utterance.onend = function(event) {
clearTimeout(timeoutResumeInfinity);
};
I discovered this by chance!
Hope this help!

The problem with Peter's answer is it doesn't work when you have a queue of speech synthesis set up. The script will put the new chunk at the end of the queue, and thus out of order. Example: https://jsfiddle.net/1gzkja90/
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>
u = new SpeechSynthesisUtterance();
$(document).ready(function () {
$('.t').each(function () {
u = new SpeechSynthesisUtterance($(this).text());
speechUtteranceChunker(u, {
chunkLength: 120
}, function () {
console.log('end');
});
});
});
/**
* Chunkify
* Google Chrome Speech Synthesis Chunking Pattern
* Fixes inconsistencies with speaking long texts in speechUtterance objects
* Licensed under the MIT License
*
* Peter Woolley and Brett Zamir
*/
var speechUtteranceChunker = function (utt, settings, callback) {
settings = settings || {};
var newUtt;
var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
newUtt = utt;
newUtt.text = txt;
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
}
if (callback !== undefined) {
callback();
}
});
}
else {
var chunkLength = (settings && settings.chunkLength) || 160;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
var chunkArr = txt.match(pattRegex);
if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
//call once all text has been spoken...
if (callback !== undefined) {
callback();
}
return;
}
var chunk = chunkArr[0];
newUtt = new SpeechSynthesisUtterance(chunk);
var x;
for (x in utt) {
if (utt.hasOwnProperty(x) && x !== 'text') {
newUtt[x] = utt[x];
}
}
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
return;
}
settings.offset = settings.offset || 0;
settings.offset += chunk.length - 1;
speechUtteranceChunker(utt, settings, callback);
});
}
if (settings.modifier) {
settings.modifier(newUtt);
}
console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
}, 0);
};
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>
In my case, the answer was to "chunk" the string before adding them to the queue. See here: http://jsfiddle.net/vqvyjzq4/
Many props to Peter for the idea as well as the regex (which I still have yet to conquer.) I'm sure the javascript can be cleaned up, this is more of a proof of concept.
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>
var chunkLength = 120;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
$(document).ready(function () {
var element = this;
var arr = [];
var txt = replaceBlank($(element).text());
while (txt.length > 0) {
arr.push(txt.match(pattRegex)[0]);
txt = txt.substring(arr[arr.length - 1].length);
}
$.each(arr, function () {
var u = new SpeechSynthesisUtterance(this.trim());
window.speechSynthesis.speak(u);
});
});
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>

Here is what i ended up with, it simply splits my sentences on the period "."
var voices = window.speechSynthesis.getVoices();
var sayit = function ()
{
var msg = new SpeechSynthesisUtterance();
msg.voice = voices[10]; // Note: some voices don't support altering params
msg.voiceURI = 'native';
msg.volume = 1; // 0 to 1
msg.rate = 1; // 0.1 to 10
msg.pitch = 2; //0 to 2
msg.lang = 'en-GB';
msg.onstart = function (event) {
console.log("started");
};
msg.onend = function(event) {
console.log('Finished in ' + event.elapsedTime + ' seconds.');
};
msg.onerror = function(event)
{
console.log('Errored ' + event);
}
msg.onpause = function (event)
{
console.log('paused ' + event);
}
msg.onboundary = function (event)
{
console.log('onboundary ' + event);
}
return msg;
}
var speekResponse = function (text)
{
speechSynthesis.cancel(); // if it errors, this clears out the error.
var sentences = text.split(".");
for (var i=0;i< sentences.length;i++)
{
var toSay = sayit();
toSay.text = sentences[i];
speechSynthesis.speak(toSay);
}
}

2017 and this bug is still around. I happen to understand this problem quite well, being the developer of the award-winning Chrome extension Read Aloud. OK, just kidding about the award winning part.
Your speech will get stuck if it's longer than 15 seconds.
I discover that Chrome uses a 15 second idle timer to decide when to deactivate an extension's event/background page. I believe this is the culprit.
The workaround I've used is a fairly complicated chunking algorithm that respects punctuation. For Latin languages, I set max chunk size at 36 words. The code is open-source, if you're inclined: https://github.com/ken107/read-aloud/blob/315f1e1d5be6b28ba47fe0c309961025521de516/js/speech.js#L212
The 36-word limit works well most of the time, staying within 15 seconds. But there'll be cases where it still gets stuck. To recover from that, I use a 16 second timer.

I ended up chunking up the text and having some intelligence around handling of various punctucations like periods, commas, etc. For example, you don't want to break the text up on a comma if it's part of a number (i.e., $10,000).
I have tested it and it seems to work on arbitrarily large sets of input and it also appears to work not just on the desktop but on android phones and iphones.
Set up a github page for the synthesizer at: https://github.com/unk1911/speech
You can see it live at: http://edeliverables.com/tts/

new Vue({
el: "#app",
data: {
text: `Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.`
},
methods:{
stop_reading() {
const synth = window.speechSynthesis;
synth.cancel();
},
talk() {
const synth = window.speechSynthesis;
const textInput = this.text;
const utterThis = new SpeechSynthesisUtterance(textInput);
utterThis.pitch = 0;
utterThis.rate = 1;
synth.speak(utterThis);
const resumeInfinity = () => {
window.speechSynthesis.resume();
const timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
}
utterThis.onstart = () => {
resumeInfinity();
};
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="talk">Speak</button>
<button #click="stop_reading">Stop</button>
</div>

As Michael proposed, Peter's solutions is really great except when your text is on different lines. Michael created demo to better illustrate the problem with it. - https://jsfiddle.net/1gzkja90/ and proposed another solution.
To add one maybe simpler way to solve this is to remove line breaks from textarea in Peter's solution and it works just great.
//javascript
var noLineBreaks = document.getElementById('mytextarea').replace(/\n/g,'');
//jquery
var noLineBreaks = $('#mytextarea').val().replace(/\n/g,'');
So in Peter's solution it might look the following way :
utterance.text = $('#mytextarea').val().replace(/\n/g,'');
But still there's problem with canceling the speech. It just goes to another sequence and won't stop.

Other suggestion do weird thing with dot or say DOT and do not respect speech intonnation on sentence end.
var CHARACTER_LIMIT = 200;
var lang = "en";
var text = "MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence. Joe waited for the train. The train was late. Mary and Samantha took the bus.";
speak(text, lang)
function speak(text, lang) {
//Support for multipart text (there is a limit on characters)
var multipartText = [];
if (text.length > CHARACTER_LIMIT) {
var tmptxt = text;
while (tmptxt.length > CHARACTER_LIMIT) {
//Split by common phrase delimiters
var p = tmptxt.search(/[:!?.;]+/);
var part = '';
//Coludn't split by priority characters, try commas
if (p == -1 || p >= CHARACTER_LIMIT) {
p = tmptxt.search(/[,]+/);
}
//Couldn't split by normal characters, then we use spaces
if (p == -1 || p >= CHARACTER_LIMIT) {
var words = tmptxt.split(' ');
for (var i = 0; i < words.length; i++) {
if (part.length + words[i].length + 1 > CHARACTER_LIMIT)
break;
part += (i != 0 ? ' ' : '') + words[i];
}
} else {
part = tmptxt.substr(0, p + 1);
}
tmptxt = tmptxt.substr(part.length, tmptxt.length - part.length);
multipartText.push(part);
//console.log(part.length + " - " + part);
}
//Add the remaining text
if (tmptxt.length > 0) {
multipartText.push(tmptxt);
}
} else {
//Small text
multipartText.push(text);
}
//Play multipart text
for (var i = 0; i < multipartText.length; i++) {
//Use SpeechSynthesis
//console.log(multipartText[i]);
//Create msg object
var msg = new SpeechSynthesisUtterance();
//msg.voice = profile.systemvoice;
//msg.voiceURI = profile.systemvoice.voiceURI;
msg.volume = 1; // 0 to 1
msg.rate = 1; // 0.1 to 10
// msg.rate = usersetting || 1; // 0.1 to 10
msg.pitch = 1; //0 to 2*/
msg.text = multipartText[i];
msg.speak = multipartText;
msg.lang = lang;
msg.onend = self.OnFinishedPlaying;
msg.onerror = function (e) {
console.log('Error');
console.log(e);
};
/*GC*/
msg.onstart = function (e) {
var curenttxt = e.currentTarget.text;
console.log(curenttxt);
//highlight(e.currentTarget.text);
//$('#showtxt').text(curenttxt);
//console.log(e);
};
//console.log(msg);
speechSynthesis.speak(msg);
}
}
https://jsfiddle.net/onigetoc/9r27Ltqz/

I want to say that through Chrome Extensions and Applications, I solved this quite irritating issue through using chrome.tts, since chrome.tts allows you to speak through the browser, instead of the window which stops the talk when you close the window.
Using the below code, you can fix the above issue with large speakings:
chrome.tts.speak("Abnormally large string, over 250 characters, etc...");
setInterval(() => { chrome.tts.resume(); }, 100);
I'm sure that will work, but I did this just to be safe:
var largeData = "";
var smallChunks = largeData.match(/.{1,250}/g);
for (var chunk of smallChunks) {
chrome.tts.speak(chunk, {'enqueue': true});
}
Hope this helps someone! It helped make my application work more functionally, and epicly.

Yes, the google synthesis api will stop at some point during speaking a long text.
We can see onend event, onpause and onerror event of SpeechSynthesisUtterance won't be fired normally when the sudden stop happens, so does the speechSynthesis onerror event.
After several trials, found speechSynthesis.paused is working, and speechSynthesis.resume() can help resume the speaking.
Hence we just need to have a timer to check the pause status during the speaking, and calling speechSynthesis.resume() to continue.
The interval should be small enough to prevent glitch when continuing the speak.
let timer = null;
let reading = false;
let readText = function(text) {
if (!reading) {
speechSynthesis.cancel();
if (timer) {
clearInterval(timer);
}
let msg = new SpeechSynthesisUtterance();
let voices = window.speechSynthesis.getVoices();
msg.voice = voices[82];
msg.voiceURI = 'native';
msg.volume = 1; // 0 to 1
msg.rate = 1.0; // 0.1 to 10
msg.pitch = 1; //0 to 2
msg.text = text;
msg.lang = 'zh-TW';
msg.onerror = function(e) {
speechSynthesis.cancel();
reading = false;
clearInterval(timer);
};
msg.onpause = function(e) {
console.log('onpause in ' + e.elapsedTime + ' seconds.');
}
msg.onend = function(e) {
console.log('onend in ' + e.elapsedTime + ' seconds.');
reading = false;
clearInterval(timer);
};
speechSynthesis.onerror = function(e) {
console.log('speechSynthesis onerror in ' + e.elapsedTime + ' seconds.');
speechSynthesis.cancel();
reading = false;
clearInterval(timer);
};
speechSynthesis.speak(msg);
timer = setInterval(function(){
if (speechSynthesis.paused) {
console.log("#continue")
speechSynthesis.resume();
}
}, 100);
reading = true;
}
}

Related

How do I stop my program from outputting the same thing twice in a row?

So, I have created this HTML page with some JavaScript in it. And I have this button that outputs one out of six emojis. It worked fine and then I added some code to stop the program from outputting the same emoji twice in a row, but it doesn't make any difference and I don't know why.
This is my code:
function randEmoji()
{
var oldEmoji = emoji;
var emojiList = [";)", ":D", "xD", ":O", ":X", ":P"];
var emoji = emojiList[Math.floor(Math.random() * emojiList.length)];
if (oldEmoji == emoji)
{
randEmoji();
}
else
{
document.getElementById("emojiText").innerHTML = "Look how fun! ---> " + emoji + " <--- An emoji!";
console.log(emoji);
}
}
I'm not very good at programming and have no idea what's causing this problem.
Please help me!
you have to declare your variables outside the function and set them inside. otherwise their values get reset in each function call.
Try this:
var oldEmoji = '';
var emoji = '';
var emojiList = [";)", ":D", "xD", ":O", ":X", ":P"];
function randEmoji() {
oldEmoji = emoji;
emoji = emojiList[Math.floor(Math.random() * emojiList.length)];
if (oldEmoji == emoji) {
randEmoji();
} else {
console.log(emoji);
}
}
var i = 0;
while (i < 20) {
randEmoji();
i += 1;
}
The problem is that any variables local to the function scope (meaning declared inside the function) are thrown away after the function completes its execution. Thus, every time you run the function emoji and oldEmoji are reinstantiated from undefined
One solution would be to move one of those declarations to a parent scope, like so:
var oldEmoji;
function randEmoji() {
var emojiList = [";)", ":D"];
var emoji = emojiList[Math.floor(Math.random() * emojiList.length)];
if (oldEmoji == emoji) {
randEmoji();
} else {
console.log(emoji);
oldEmoji = emoji;
}
}
randEmoji();
randEmoji();
randEmoji();
randEmoji();
randEmoji();
randEmoji();
randEmoji();
See here, we never actually get a repeat.
In addition to #full-stack answer, you could:
var oldEmoji = '';
var emoji = '';
var emojiList = [";)", ":D", "xD", ":O", ":X", ":P"];
function randEmoji(){
// remove old emoji first to avoid doing a recursive call
var check = oldEmoji? emojiList.filter(e => e !== oldEmoji ) : emojiList;
var emoji = check[Math.floor(Math.random() * check.length)];
oldEmoji = emoji
document.getElementById("emojiText").innerHTML = "Look how fun! ---> " + emoji + " <--- An emoji!";
console.log(emoji);
}

(Scripting) Photoshop removes special characters

I have a script (with a lot of stolen parts you may recognise) that runs through a selected group of images, copies the image and filename and applies to a template in Photoshop. Everything works just fine, except that Photoshop somehow strips umlauts from my strings, ie, Björn becomes Bjorn.
"Logging" through an alert inside of Photoshop (line #30 below) shows that it has the correct string all the way until it's applied as the textItem.contents.
Code provided below, thanks for any help!
#target photoshop
app.displayDialogs = DialogModes.NO;
var templateRef = app.activeDocument;
var templatePath = templateRef.path;
var photo = app.activeDocument.layers.getByName("Photo"); // keycard_template.psd is the active document
// Check if photo layer is SmartObject;
if (photo.kind != "LayerKind.SMARTOBJECT") {
alert("selected layer is not a smart object")
} else {
// Select Files;
if ($.os.search(/windows/i) != -1) {
var photos = File.openDialog("Select photos", "*.png;*.jpg", true)
} else {
var photos = File.openDialog("Select photos", getPhotos, true)
};
if (photos.length) replaceItems();
}
function replaceItems() {
for (var m = 0; m < photos.length; m++) {
if (photos.length > 0) {
// Extract name
var nameStr = photos[m].name;
var nameNoExt = nameStr.split(".");
var name = nameNoExt[0].replace(/\_/g, " ");
// Replace photo and text in template
photo = replacePhoto(photos[m], photo);
// alert(name);
replaceText(templateRef, 'Name', name);
templateRef.saveAs((new File(templatePath + "/keycards/" + name + ".jpg")), jpgOptions, true);
}
}
}
// OS X file picker
function getPhotos(thePhoto) {
if (thePhoto.name.match(/\.(png|jpg)$/i) != null || thePhoto.constructor.name == "Folder") {
return true
};
};
// JPG output options;
var jpgOptions = new JPEGSaveOptions();
jpgOptions.quality = 12; //enter number or create a variable to set quality
jpgOptions.embedColorProfile = true;
jpgOptions.formatOptions = FormatOptions.STANDARDBASELINE;
// Replace SmartObject Contents
function replacePhoto(newFile, theSO) {
app.activeDocument.activeLayer = theSO;
// =======================================================
var idplacedLayerReplaceContents = stringIDToTypeID("placedLayerReplaceContents");
var desc3 = new ActionDescriptor();
var idnull = charIDToTypeID("null");
desc3.putPath(idnull, new File(newFile));
var idPgNm = charIDToTypeID("PgNm");
desc3.putInteger(idPgNm, 1);
executeAction(idplacedLayerReplaceContents, desc3, DialogModes.NO);
return app.activeDocument.activeLayer
};
// Replace text strings
function replaceText(doc, layerName, newTextString) {
for (var i = 0, max = doc.layers.length; i < max; i++) {
var layerRef = doc.layers[i];
if (layerRef.typename === "ArtLayer") {
if (layerRef.name === layerName && layerRef.kind === LayerKind.TEXT) {
layerRef.textItem.contents = decodeURI(newTextString);
}
} else {
replaceText(layerRef, layerName, newTextString);
}
}
}
Had the same problem and tried everything I had in my developer toolbox... for around 3 hours ! without any success and then I found a little hack !
It seems that photoshop is uri encoding the name of the file but don't do it in a way that allow us to do a decodeURI() and get back those special characters.
For exemple instead of "%C3%A9" that should be "é" we get "e%CC%81". So what i do is a replace on the file name to the right UTF-8 code and then a decodeURI. Exemple :
var fileName = file.name
var result = fileName.replace(/e%CC%81/g, '%C3%A9') // allow é in file name
var myTextLayer.contents = decodeURI(result);
Then you can successfully get special chars and in my case accent from filename in your text layer.
You can do a replace for each characters you need for me it was :
'e%CC%81': '%C3%A9', //é
'o%CC%82': '%C3%B4', //ô
'e%CC%80': '%C3%A8', //è
'u%CC%80': '%C3%B9', //ù
'a%CC%80': '%C3%A0', //à
'e%CC%82': '%C3%AA' //ê
I took UTF-8 code from this HTML URL Encoding reference : https://www.w3schools.com/tags/ref_urlencode.asp
Hope it will help somebody one day because nothing existed online on this bug.

Javascript - register how long user pressed a given button during whole visit

I'm programming an experiment in Qualtrics and I basically need to create, with Javascript, a variable that tells me how long (total) participants held down any button. Pressing a button will display text to the participants and releasing it will make the text disappear, so basically it tells me how much time they had to read the text. So let's say they press the button three times, first for 30 seconds, second for 10 seconds and third for 2 seconds, this code should store the value 42.
What I have so far is this:
addEventListener("keydown", function(event) {
if (event.keyCode == 86)
document.getElementById("text").innerHTML = "Text to show";
var d1 = new Date();
document.getElementById("div1").innerHTML = d1.getTime();
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86)
document.getElementById("text").innerHTML = "";
var d2 = new Data();
var d1 = parseFloat(document.getElementById("div1"));
var diff = d2 - d1.getTime();
var old = parseFloat(document.getElementById("div2"));
var old = old + diff;
document.getElementById("div2").innerHTML = old;
Qualtrics.SurveyEngine.setEmbeddedData("readingtime", totalTime);
});
I store the values in divs because I can't seem to reuse values from one event listener to the other (this is probably because I don't know enough about javascript and scopes). Oh, and the last function is just a Qualtrics-specific function to store the value in the database. Anyway, I can't get it to work, when I check the variable in the database it is simply empty. Anyone can spot what I'm doing wrong?
I did a few changes to your code:
Added global variables
Added few missing brackets
Attached listeners to window
Removed multiple calls to DOM elements
Created a function for each event listener
Rounded the elapsed time to seconds
var d0;
var d1;
var subtotal = 0;
var total = 0;
var div1 = document.getElementById("div1");
var text = document.getElementById("text");
window.addEventListener("keydown", dealWithKeyDown, false);
window.addEventListener("keyup", dealWithKeyUp, false);
function dealWithKeyDown(event) {
if (event.keyCode == 86) {
if (typeof d0 === 'undefined') {
d0 = new Date();
}
d1 = new Date();
subtotal = Math.round((d1.getTime() - d0.getTime()) / 1000);
div1.innerHTML = subtotal;
text.innerHTML = "Text to show";
}
}
function dealWithKeyUp(event) {
if (event.keyCode == 86) {
total = total + subtotal;
text.innerHTML = "";
d0 = undefined;
Qualtrics.SurveyEngine.setEmbeddedData("readingtime", total);
}
}
Okey dokey, since none of the posted answers seem to have been accepted, I'm gonna post my own.
There's really not that much to say about this solution, it's as easy as it gets, nicely put into objects, so that we know what's going on, and I am even giving you a fiddle for it!
There's one problem I don't know if I solved or not, but sometimes the button will show that it has been pressed for millions of seconds, I think that's because Key is not being initialized properly, which is pretty weird, but it happens rarely enough for me to put the burden of fixing it on you.
The code is here:
https://jsfiddle.net/vo2n1jw1/
Pasted over:
var Key = function(code)
{
this.code = code;
};
Key.prototype.time = 0;
Key.prototype.pressedAt = 0;
Key.prototype.getTimeInSeconds = function()
{
return this.time / 1000;
};
var Keyboard = function()
{
this.keys = [];
};
Keyboard.prototype.addOrGetKey = function(code)
{
var key = this.getKey(code);
if(!key)
{
key = new Key(code);
this.addKey(key);
}
return key;
};
Keyboard.prototype.addKey = function(key)
{
this.getKeys()[key.code] = key;
};
Keyboard.prototype.getKey = function(code)
{
return this.getKeys()[code];
};
Keyboard.prototype.getKeys = function()
{
return this.keys;
};
Keyboard.prototype.printAllKeysIntoElement = function(element)
{
var keys = this.getKeys();
var length = keys.length;
element.innerHTML = "";
for(var i = 0; i < length; i++)
{
var key = keys[i];
if(!key)
{
continue;
}
var keyElement = document.createElement("div");
keyElement.innerHTML = "Button: " + key.code + " has been pressed for " + key.getTimeInSeconds() + " seconds";
element.appendChild(keyElement);
}
};
var KeyboardListener = function(keyboard, element)
{
this.keyboard = keyboard;
this.container = element;
this.onKeyDownThis = this.onKeyDown.bind(this);
document.addEventListener("keydown", this.onKeyDownThis, false);
document.addEventListener("keyup", this.onKeyUp.bind(this), false);
};
KeyboardListener.prototype.onKeyDown = function(event)
{
console.log("press");
var keyboard = this.getKeyboard();
var code = event.keyCode;
var key = keyboard.addOrGetKey(code);
key.pressedAt = Date.now();
document.removeEventListener("keydown", this.onKeyDownThis, false);
return false;
};
KeyboardListener.prototype.onKeyUp = function(event)
{
console.log("release");
var keyboard = this.getKeyboard();
var code = event.keyCode;
var key = keyboard.addOrGetKey(code);
if(key.pressedAt)
{
key.time += Date.now() - key.pressedAt;
keyboard.printAllKeysIntoElement(this.container);
}
document.addEventListener("keydown", this.onKeyDownThis, false);
return false;
};
KeyboardListener.prototype.getKeyboard = function()
{
return this.keyboard;
};
var resultsElement = document.getElementById("results");
var keyboard = new Keyboard();
var listener = new KeyboardListener(keyboard, resultsElement);
There's 3 objects:
Key
Keyboard
KeyboardListener
They really do what they sound like.
Tell me, if you want anything explained.
Oh, one thing, I know you're not supposed to use arrays like this, but I was lazy.
You can try to create a variable outside both listeners, and outside functions, so you can use it anywhere.
And I think you are doing well by getting the times of Dates.
var firstTime = new Date();
var totalTime = 0;
addEventListener("keydown", function(event) {
if (event.keyCode == 86) {
document.getElementById("text").innerHTML = "Text to show";
firstTime = new Date();
}
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86) {
document.getElementById("text").innerHTML = "";
var d2 = new Date();
var diff = d2.getTime() - firstTime.getTime();
totalTime += diff;
Qualtrics.SurveyEngine.setEmbeddedData("readingtime", totalTime);
}
});
It's better to declare d1 & d2 variables global instead of using html elements as storage.
var d1, d2;
var total_time = 0;
var key_pressed = false;
addEventListener("keydown", function(event) {
if (event.keyCode == 86) {
if (!key_pressed)
d1 = new Date();
key_pressed = true;
}
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86) {
key_pressed = false;
d2 = new Date();
total_time += d2.getTime() - d1.getTime();
Qualtrics.SurveyEngine.setEmbeddedData("readingtime", total_time);
}
});
When the 'v' key is pressed and hold, keydown listener is called repeatedly. That's why there is boolean variable i.e. "key_pressed" to detect hold key first time.
Where does the "totalTime" get assigned?
Is your totalTime has any value?
Did you try to debug the javascript using the browser console?
If you don't want to store data to divs, u can store it within global vars.
var times = {
start: 0,
total: 0
}
addEventListener("keydown", function(event) {
if (event.keyCode == 86) {
// i think your hole code should be executed only on releasing this one button
document.getElementById("text").innerHTML = "Text to show";
times.start = new Date();
}
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86) {
// i think your hole code should be executed only on releasing this one button
document.getElementById("text").innerHTML = "";
var diff = new Date().getTime() - times.start.getTime();
times.total = times.total + diff;
Qualtrics.SurveyEngine.setEmbeddedData("readingtime", times.total);
}
});
So you can use the var times outside of the event listener function to declare it global.
I noticed that you used the if ().. without { and }, so only the next line is affected, im dont shure if you want that.

Indesign JavaScript Creating Text & intra-doc Hyperlinks in Book - Extremely Slow

First time posting
First time writing in JavaScript, though I have experience in other languages.
I'm working in Adobe InDesign CS5.5. I have multiple files in an ID Book, each containing a varying number of "chapters". The book includes an index file with topic headings that reference the chapters in an abbreviated form (e.g., "CHAPTER 125" becomes "ch 125 no 3" -- note the "no x" part is irrelevant). The goal of my script is to create inter-document links that will add significant functionality when the ID Book is exported to, say, a PDF. The user will be able to jump from index to chapter and vice-versa. I think the script and the issues I'm dealing with would be of use to others but haven't found any posts to address my problem yet.
All refs (like "ch 125 no 1") in the index to a particular chapter ("CHAPTER 125") get a hyperlink to the location of the head of that chapter. This part of the script is working great and runs quickly.
The other half will insert the corresponding topic headings at the end of each chapter text and make those paragraphs link back to the corresponding topic head in the index. (In other words, they are cross references but not true x-refs in ID terms because I wanted more control over them and my reading on the topic told me to steer clear of true x-refs.) This is the part of the script that has me banging my head on the wall. It runs for hours upon hours without finishing a book of 200 chapters. Note that for testing purposes I am simply inserting one paragraph of text in the desired location under each chapter, rather than all topic heads and links. I know from smaller sets of text and from my debugging prints to the console that the script is doing work, not stuck in an infinite loop. Nevertheless, it runs way too long and, if I interrupt it, InDesign is unresponsive and I have to kill it, so cannot even review the partial results.
Based on searching/reading forums: I have disabled preflighting; disabled auto updating of book page numbers; changed the live preview settings to delayed. I still suspect the slowness may have to do with InDesign overhead but I don't know what else to try.
I'm embarrassed at how awful the style of this JS code might be but at the moment I just need it to work, then I can refine it.
var myBookFilePath = File.openDialog("Choose an InDesign Book File", "Indb files: *.indb");
var myOpenBook = app.open(myBookFilePath);
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
// Open up every file in the currently active Book
app.open(app.activeBook.bookContents.everyItem().fullName)
// TODO: add error handling / user interaction here -- to pick which is Index file
var strIndexFilename = "Index.indd";
var objChapHeadsWeb = {};
var myDoc = app.documents.item(strIndexFilename);
$.writeln("\n\n~~~ " + myDoc.name + " ~~~");
// REMOVED CODE - check for existing hyperlinks, hyperlink sources/destinations
// loop to delete any pre-existing hyperlinks & associated objects
// works w/o any problems
// Ugly GREP to find the Main heading text (all caps entry and nothing beyond) in the index file
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
/// GREP: ^[\u\d \:\;\?\-\'\"\$\%\&\!\#\*\#\,\.\(\)]+[\u\d](?=\.|,)
app.findGrepPreferences.findWhat = "^[\\u\\d \\:\\;\\?\\-\\'\\\"\\$\\%\\&\\!\\#\\*\\#\\,\\.\\(\\)]+[\\u\\d](?=\\.|,)";
app.findGrepPreferences.appliedParagraphStyle = "Main";
var myFound = [];
myFound = myDoc.findGrep();
$.writeln("Found " + myFound.length + " Main headings.");
for (var i = 0; i < myFound.length; i++) {
myDoc.hyperlinkTextDestinations.add(myFound[i], { name: myFound[i].contents });
}
$.writeln("There are now " + myDoc.hyperlinkTextDestinations.count() + " destinations.");
myFound.length = 0;
for (var j = app.documents.count()-1; j >= 0; j--) {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
// set the variable to the document we are working with
myDoc = null;
myDoc = app.documents[j];
myFound.length = 0;
if (myDoc.name === strIndexFilename) {
continue; // we don't want to look for chapter heads in the Index file, so skip it
}
$.writeln("\n\n~~~ " + myDoc.name + " ~~~");
// REMOVED CODE - check for existing hyperlinks, hyperlink sources/destinations
// loop to delete any pre-existing hyperlinks & associated objects
// works w/o any problems
// Clear GREP prefs
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "^CHAPTER \\d+";
app.findGrepPreferences.appliedParagraphStyle = "chapter";
myFound = myDoc.findGrep();
var strTemp = "";
$.writeln("Found " + myFound.length + " chapter headings.");
for (var m = 0; m < myFound.length; m++) {
strTemp = myFound[m].contents;
objChapHeadsWeb[strTemp] = {};
objChapHeadsWeb[strTemp].withinDocName = myDoc.name;
objChapHeadsWeb[strTemp].hltdChHead =
myDoc.hyperlinkTextDestinations.add(myFound[m], {name:strTemp});
objChapHeadsWeb[strTemp].a_strIxMains = [];
objChapHeadsWeb[strTemp].a_hltdIxMains = [];
objChapHeadsWeb[strTemp].nextKeyName = "";
objChapHeadsWeb[strTemp].nextKeyName =
((m < myFound.length-1) ? myFound[m+1].contents : String(""));
}
$.writeln("There are now " + myDoc.hyperlinkTextDestinations.count() + " destinations.");
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Find the "ch" (chapter) references in the index file, link them
// back to the corresponding text anchors for the chapter heads
// in the text.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
myDoc = app.documents.item(strIndexFilename); // work with the Index file
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
// GREP to find the "ch" (chapter) references in the index file
// like ch 151 no 1 OR ch 12 no 3
app.findGrepPreferences.findWhat = "(ch\\s+\\d+\\s+no\\s+\\d+)";
var strExpandedChap = "";
var strWorkingMainHd = "";
var arrFoundChapRefs = [];
var myHyperlinkSource;
var myHyperlinkDest;
for (var x = 0; x < myDoc.hyperlinkTextDestinations.count(); x++) {
strWorkingMainHd = "";
arrFoundChapRefs.length = 0;
// the special case, where we are working with the ultimate hyperlinkTextDestination obj
if (x === myDoc.hyperlinkTextDestinations.count()-1) {
// This is selecting text from the start of one MAIN heading...
myDoc.hyperlinkTextDestinations[x].destinationText.select();
// This next line will extend the selection to the end of the story,
// which should also be the end of the document
myDoc.selection[0].parentStory.insertionPoints[-1].select(SelectionOptions.ADD_TO);
}
// the regular case...
else {
// This is selecting text from the start of one MAIN heading...
myDoc.hyperlinkTextDestinations[x].destinationText.select();
// ... to the start of the next MAIN heading
myDoc.hyperlinkTextDestinations[x+1].destinationText.select(SelectionOptions.ADD_TO);
}
strWorkingMainHd = myDoc.hyperlinkTextDestinations[x].name;
//arrFoundChapRefs = myDoc.selection[0].match(/(ch\s+)(\d+)(\s+no\s+\d+)/g); //NOTE: global flag
arrFoundChapRefs = myDoc.selection[0].findGrep();
for(y = 0; y < arrFoundChapRefs.length; y++) {
myHyperlinkSource = null;
myHyperlinkDest = null;
strExpandedChap = "";
strExpandedChap = arrFoundChapRefs[y].contents.replace(/ch\s+/, "CHAPTER ");
strExpandedChap = strExpandedChap.replace(/\s+no\s+\d+/, "");
// if we found the chapter head corresponding to our chapter ref in the index
// then it is time to create a link
if (strExpandedChap in objChapHeadsWeb) {
objChapHeadsWeb[strExpandedChap].a_strIxMains.push(strWorkingMainHd);
objChapHeadsWeb[strExpandedChap].a_hltdIxMains.push(myDoc.hyperlinkTextDestinations[x]);
myHyperlinkSource = myDoc.hyperlinkTextSources.add(arrFoundChapRefs[y]);
myHyperlinkDest = objChapHeadsWeb[strExpandedChap].hltdChHead;
myDoc.hyperlinks.add(myHyperlinkSource, myHyperlinkDest);
} else {
$.writeln("Couldn't find chapter head " + strExpandedChap);
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// NOW TIME FOR THE HARD PART...
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
myDoc = null;
var strWorkingMainHd = "";
var nextKey = "";
var myParentStory = null;
var myCharIndex = 0;
var myCompareChar = null;
var myLeftmostBound = 0;
var myCurrentPara = null;
for (var key in objChapHeadsWeb) {
myDoc = app.documents.item(objChapHeadsWeb[key].withinDocName);
myCompareChar = null; //recent addition
$.writeln("Working on " + key + "."); //debugging
nextKey = objChapHeadsWeb[key].nextKeyName;
objChapHeadsWeb[key].hltdChHead.destinationText.select();
myLeftmostBound = myDoc.selection[0].index;
myParentStory = myDoc.selection[0].parentStory;
if( (nextKey === "") || (myDoc.name !== objChapHeadsWeb[nextKey].withinDocName) )
{
//// Need to find end of story instead of beginning of next chapter
//myDoc.selection[0].parentStory.insertionPoints[-1].select(SelectionOptions.ADD_TO);
myParentStory.insertionPoints[-1].select();
//myCharIndex = myDoc.selection[0].index; /recently commented out
myCharIndex = myDoc.selection[0].index - 1; //testing new version
myCompareChar = myParentStory.characters.item(myCharIndex); //recenttly added/relocated from below
} else {
/////
//objChapHeadsWeb[nextKey].hltdChHead.destinationText.select(SelectionOptions.ADD_TO);
objChapHeadsWeb[nextKey].hltdChHead.destinationText.select();
//myParentStory.characters.item(myDoc.selection[0].index -1).select();
myParentStory.characters.item(myDoc.selection[0].index -2).select(); //temp test *****
myCharIndex = myDoc.selection[0].index;
myCompareChar = myParentStory.characters.item(myCharIndex);
if (myCompareChar.contents === "\uFEFF") {
$.writeln("Message from inside the \\uFEFF check."); //debugging
myParentStory.characters.item(myDoc.selection[0].index -1).select();
myCharIndex = myDoc.selection[0].index;
myCompareChar = myParentStory.characters.item(myCharIndex);
}
if( (myCompareChar.contents !== SpecialCharacters.PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.ODD_PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.EVEN_PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.COLUMN_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.FRAME_BREAK))
{
$.writeln("Possible error finding correct insertion point for " + objChapHeadsWeb[key].hltdChHead.name + ".");
}
}
if(myCharIndex <= myLeftmostBound) { // this shouldn't ever happen
alert("Critical error finding IX Marker insertion point for " + objChapHeadsWeb[key].hltdChHead.name + ".");
}
if(myCompareChar.contents !== "\r") {
myDoc.selection[0].insertionPoints[-1].contents = "\r";
}
myDoc.selection[0].insertionPoints[-1].contents = "TESTING text insertion for: " + objChapHeadsWeb[key].hltdChHead.name + "\r";
myDoc.selection[0].insertionPoints.previousItem(myDoc.selection[0].insertionPoints[-1]).select();
//myDoc.selection[0].insertionPoints[-1].contents = "<Now I'm here!>";
myCurrentPara = myDoc.selection[0].paragraphs[0];
myCurrentPara.appliedParagraphStyle = myDoc.paragraphStyles.item("IX Marker");
// TODO:
// need error handling for when style doesn't already exist in the document
} // end big for loop
//TODO: add error handling support to carry on if user cancels
//close each open file; user should be prompted to save changed files by default
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
app.documents.everyItem().close();
// Cleanup
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
Try open all files of Cross References are linking to them.
May I suggest a few improvements that can probably speed up things a bit.
First of all you have tons of global variables here that you may concentrate in much less scopes using functions. Having many global variables has a big cost in terms of performances.
Once that said, I won't open every single doc of the book at once but process them one by one. Be aware that grep calls are very cost expensive so you may try to look at your patterns.
Another one is the extensive use of the $.writeln command. Avoid it especially within loops. Prefer an easy to set report library.
Finally I tried to rewrite your code in a "better" way but it was hard to construct the whole script with a clear understanding of your needs and no files to process. But I hope the following snippet will help you to start rewriting your code and state significant time improvements.
var debug = true;
var log = function(msg) {
var l = File (Folder.desktop+"/log.txt" );
if ( !debug ) return;
l.open('a');
l.write(msg);
l.close();
};
var main = function() {
var bookFile, uil = app.scriptPreferences.userIntercationLevel;
log("The party has started");
bookFile = File.openDialog("Choose an InDesign Book File", "Indb files: *.indb");
if (!bookFile) return;
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.NEVER_INTERACT;
try {
processBookFile ( bookFile );
}
catch(err) {
alert(err.line+"///"+err.message);
}
app.scriptPreferences.userInteractionLevel = uil;
};
function processBookFile ( bookFile ) {
var book = app.open ( bookFile ),
bks = book.bookContents,
n = bks.length;
while ( n-- ) {
File(bks[n].name)!="Index.indd" && processBookContent ( bks[n] );
}
}
function processBookContent ( bookContent ) {
var bcf = bookContent.fullName,
doc = app.open ( bcf, debug );
//DEAL WITH HEADINGS
processHeadings ( doc );
//DEAL WITH CHAPTERS
processHeadings ( doc );
//add hyperlinks
addHyperlinks( doc);
}
function processHeadings (doc){
var props = {
findWhat : "^[\\u\\d \\:\\;\\?\\-\\'\\\"\\$\\%\\&\\!\\#\\*\\#\\,\\.\\(\\)]+[\\u\\d](?=\\.|,)",
appliedParagraphStyle : "Main"
},
found = findGrep(doc, props),
n = found.length;
while ( n-- ) {
doc.hyperlinkTextDestinations.add(doc, { name: found[i].contents });
}
};
function processChapters (doc ) {
var props = {
findWhat : "^CHAPTER \\d+",
appliedParagraphStyle : "chapter"
},
found = findGrep(doc, props),
n = found.length;
while ( n-- ) {
doc.hyperlinkTextDestinations.add(found[n], found[n].contents);
}
}
function findGrep(doc, props){
app.findGrepPreferences = app.changeGrepPreferences = null;
app.findGrepPreferences.properties = props;
return doc.findGrep();
}
function addHyperlinks (doc){
//a logic of yours
};
main();

Something horribly wrong with my js code, anyone can help me debug?

I'm trying to write this tool, to animate a game map from a range of dates. The flow is like this:
1st: choose game world
2nd: set map display parameters (date range, map type and animation speed)
3rd: the js code grab png file according to the dates and display them one by one according to the animation speed
The problem I'm having is this:
if you just click on one world, and click animate, everything is fine, the animation displays correctly. Then if you choose another world (without refreshing the page), the animation either flickers or somehow displaying image from other worlds. and I can't figure out what's causing this (I'm totally n00b at js)
$(function(){
$("#image_area").hide();
$("#tw_image").hide();
$('#W40').click(function(){
$("#tw_image").hide();
show_image_area('40');
});
$('#W42').click(function(){
$("#tw_image").hide();
show_image_area('42');
});
$('#W56').click(function(){
$("#tw_image").hide();
show_image_area('56');
});
});
function show_image_area(world){
$("#tw_image").hide();
if(world == "40" || world == "42" || world == "56"){
$("#map_notice").remove();
$("#image_area").prepend('<div id="map_notice">Map for W'+world+' available from <span id="date_highlight">April 7th 2011</span>, all previous dates invalid and will not have map available</div>');
}
$("#date_from").datepicker({ showAnim: 'blind' });
$("#date_to").datepicker({ showAnim: 'blind' });
$('#image_area').show();
$("#animate").click(function(){
$("#tw_image").hide();
var date_from = $("#date_from").val();
var date_to = $("#date_to").val();
if(!(date_from && date_to)){
alert("From and To dates required.")
return false;
}
var map_type = $("#map_type").val();
var speed = $("#speed").val();
var days = daydiff(parseDate(date_from), parseDate(date_to));
var date_one = new Date(Date.parse(date_from));
var b = date_one.toISOString().split("T")[0].split("-");
var c = get_map_type(map_type) + b[0] + b[1] + b[2];
var width = get_map_type_width(map_type);
var img_load = "" + world + "/" + c + ".png";
$('#image_area').load(img_load, function(){
$("#tw_image").attr("width", width);
$("#tw_image").attr("src", img_load);
$("#tw_image").show();
$("#debug").html("world = "+world);
});
var i = 0;
var interval = setInterval(
function(){
date_one.setDate(date_one.getDate() + 1);
b = date_one.toISOString().split("T")[0].split("-");
c = get_map_type(map_type) + b[0] + b[1] + b[2];
var src_one = "" + world + "/"+c+".png";
var img = new Image();
img.src = src_one;
img.onload = function(){
$("#tw_image").attr("src", img.src);
$("#debug").html("world = "+world);
}
i++;
if(i >= days) clearInterval(interval);
},speed);
});
}
function get_map_type(map_type){
if(map_type == "topk"){
return "topktribes";
}
if(map_type == "toptribe"){
return "toptribes";
}
if(map_type == "topnoble"){
return "topnoblers";
}
}
function get_map_type_width(map_type){
if(map_type == "topk"){
return 1000;
}
if(map_type == "toptribe"){
return 1300;
}
if(map_type == "topnoble"){
return 1300;
}
}
function parseDate(str) {
var mdy = str.split('/');
return new Date(mdy[2], mdy[0]-1, mdy[1]);
}
function daydiff(first, second) {
return (second-first)/(1000*60*60*24)
}
Ok I think I have a solution here although its not what I thought it was going to be. Basically every time you are calling image_area_world you are creating a new click handler on the animate button. Due to the way JavaScript scope works the World variable is kept the same for that click handler at the point of creation.
Anyway to solve this problem what you can try is this just before you define your click handler.
$("#animate").unbind("click");
$("#animate").click(function () { *code* }
A couple of tools to help you out.
Visual Event
Firebug
Also a bit explaining how JavaScript Scope and Closures work

Categories