').addClass('bar'));
if (classes) {
resultDiv.addClass(classes);
}
parent.append(resultDiv);
}
});
$(input).remove();
break;
}
});
// only start counting and sizing bars if we actually have usable data
if(data) {
// number of unique responses
var total = 0;
// double loop so we can handle re-renderings of the form
$(element).find('.item').each(function(index, item) {
var name = $(item).attr('data-value');
if(key in data) {
var count = data[key]['responses'][name];
total = data[key]['count'];
}
});
// insert the total into the counter label
$(element).next('.count').each(function(index, icount) {
$(icount).text(total);
});
var oldTotal = $(element).attr('data-total');
$(element).find('.item').each(function() {
var name = $(this).attr('data-value');
var oldCount = $(this).attr('data-count');
if(key in data) {
var count = data[key]['responses'][name] || 0;
}
else {
var count = 0;
}
if(count != oldCount || total != oldTotal) {
var percent = (total) ? ((count/total)*100) + '%' : '0%';
$(this).attr('data-count', count);
$(this).find('.bar').animate({width: percent});
}
});
// record the old total value so we only animate when it changes
$(element).attr('data-total', total);
}
$(element).addClass('rendered');
});
});
}
function connectControlChannel() {
if (interactive) {
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
ws = new WebSocket(protocol + location.host + '/control');
ws.onopen = function() { connected(); };
ws.onclose = function() { disconnected(); }
ws.onmessage = function(m) { parseMessage(m.data); };
}
else {
ws = {}
ws.send = function() { /* no-op */ }
}
}
// This exists as an intermediary simply so the presenter view can override it
function reconnectControlChannel() {
connectControlChannel();
}
function connected() {
console.log('Control socket opened');
$("#feedbackSidebar .interactive").removeClass("disabled");
$("img#disconnected").hide();
try {
// If we are a presenter, then remind the server who we are
register();
}
catch (e) {}
}
function disconnected() {
console.log('Control socket closed');
$("#feedbackSidebar .interactive").addClass("disabled");
$("img#disconnected").show();
setTimeout(function() { reconnectControlChannel() } , 5000);
}
function generateGuid() {
var result, i, j;
result = 'S';
for(j=0; j<32; j++) {
if( j == 8 || j == 12|| j == 16|| j == 20)
result = result + '-';
i = Math.floor(Math.random()*16).toString(16).toUpperCase();
result = result + i;
}
return result;
}
function parseMessage(data) {
var command = JSON.parse(data);
if ("id" in command) {
var guid = command['id'];
if (lastMessageGuid != guid) {
lastMessageGuid = guid;
}
else {
return;
}
}
try {
switch (command['message']) {
case 'current':
follow(command["current"], command["increment"]);
break;
case 'complete':
completeQuestion(command["questionID"]);
break;
case 'pace':
paceFeedback(command["pace"]);
break;
case 'question':
postQuestion(command["question"], command["questionID"]);
break;
case 'cancel':
removeQuestion(command["questionID"]);
break;
case 'activity':
updateActivityCompletion(command['count']);
case 'annotation':
invokeAnnotation(command["type"], command["x"], command["y"]);
break;
case 'annotationConfig':
setting = command['setting'];
value = command['value'];
annotations[setting] = value;
break;
}
}
catch(e) {
console.log("Not a presenter! " + e);
}
}
function sendPace(pace) {
if (ws.readyState == WebSocket.OPEN) {
ws.send(JSON.stringify({ message: 'pace', pace: pace}));
}
}
function askQuestion(question) {
if (ws.readyState == WebSocket.OPEN) {
var questionID = generateGuid();
ws.send(JSON.stringify({ message: 'question', question: question, questionID: questionID}));
return questionID;
}
}
function cancelQuestion(questionID) {
if (ws.readyState == WebSocket.OPEN) {
ws.send(JSON.stringify({ message: 'cancel', questionID: questionID}));
}
}
function completeQuestion(questionID) {
var question = $("li#"+questionID)
if(question.length > 0) {
question.addClass('closed');
feedbackActivity();
}
}
function sendFeedback(rating, feedback) {
if (ws.readyState == WebSocket.OPEN) {
var slide = $("#slideFilename").text();
ws.send(JSON.stringify({ message: 'feedback', rating: rating, feedback: feedback, slide: slide}));
$("input:radio[name=rating]:checked").attr('checked', false);
}
}
function sendAnnotation(type, x, y) {
if (ws.readyState == WebSocket.OPEN) {
ws.send(JSON.stringify({ message: 'annotation', type: type, x: x, y: y }));
}
}
function sendAnnotationConfig(setting, value) {
if (ws.readyState == WebSocket.OPEN) {
ws.send(JSON.stringify({ message: 'annotationConfig', setting: setting, value: value }));
}
}
function sendActivityStatus(status) {
if (ws.readyState == WebSocket.OPEN) {
ws.send(JSON.stringify({ message: 'activity', slide: slidenum, status: status }));
}
}
function invokeAnnotation(type, x, y) {
switch (type) {
case 'erase':
annotations.erase();
break;
case 'draw':
annotations.draw(x,y);
break;
case 'click':
annotations.click(x,y);
break;
}
}
function feedbackActivity() {
$('#hamburger').addClass('highlight');
setTimeout(function() { $("#hamburger").removeClass('highlight') }, 75);
}
function track(current) {
if (mode.track && ws.readyState == WebSocket.OPEN) {
var slideName = $("#slideFilename").text() || $("#slideFile").text(); // yey for consistency
if(current) {
ws.send(JSON.stringify({ message: 'track', slide: slideName}));
}
else {
var slideEndTime = new Date().getTime();
var elapsedTime = slideEndTime - slideStartTime;
// reset the timer
slideStartTime = slideEndTime;
if (elapsedTime > 1000) {
elapsedTime /= 1000;
ws.send(JSON.stringify({ message: 'track', slide: slideName, time: elapsedTime}));
}
}
}
}
// Open a new tab with an online code editor, if so configured
function editSlide() {
var slide = $("span#slideFilename").text().replace(/\/\d+$/, '');
var link = editUrl + slide + ".md";
window.open(link);
}
function follow(slide, newIncrement) {
if (mode.follow && ! activityIncomplete) {
var lastSlide = slidenum;
console.log("New slide: " + slide);
gotoSlide(slide);
if( ! $("body").hasClass("presenter") ) {
switch (slidenum - lastSlide) {
case -1:
fireEvent("showoff:prev");
break;
case 1:
fireEvent("showoff:next");
break;
}
// if the master says we're incrementing. Use a loop in case the viewer is out of sync
while(newIncrement > incrCurr) {
increment();
}
}
}
}
function getPosition() {
// get the current position from the server
ws.send(JSON.stringify({ message: 'position' }));
}
function fireEvent(ev) {
var event = jQuery.Event(ev);
$(currentSlide).find(".content").trigger(event);
if (event.isDefaultPrevented()) {
return;
}
}
function increment() {
showIncremental(incrCurr);
var incrEvent = jQuery.Event("showoff:incr");
incrEvent.slidenum = slidenum;
incrEvent.incr = incrCurr;
$(currentSlide).find(".content").trigger(incrEvent);
incrCurr++;
}
function prevStep(updatepv)
{
$(currentSlide).find('video').each(function() {
console.log('Pausing videos on ' + currentSlide.attr('id'))
$(this).get(0).pause();
});
fireEvent("showoff:prev");
track();
slidenum--;
return showSlide(true, updatepv); // We show the slide fully loaded
}
function nextStep(updatepv)
{
$(currentSlide).find('video').each(function() {
console.log('Pausing videos on ' + currentSlide.attr('id'))
$(this).get(0).pause();
});
fireEvent("showoff:next");
track();
if (incrCurr >= incrSteps) {
slidenum++;
return showSlide(false, updatepv);
} else {
increment();
}
}
// carrying on our grand tradition of overwriting functions of the same name with presenter.js
function postSlide() {
if(currentSlide) {
var notes = getCurrentNotes();
// Replace notes with empty string if there are no notes
// Otherwise it fails silently and does not remove old notes
if (notes.length === 0) {
notes = "";
} else {
notes = notes.html();
}
$('#notes').html(notes);
// tell Showoff what slide we ended up on
track(true);
}
}
function doDebugStuff()
{
if (debugMode) {
$('#debugInfo').show();
$('#slideFilename').show();
} else {
$('#debugInfo').hide();
$('#slideFilename').hide();
}
}
function blankScreen()
{
try {
slaveWindow.blankScreen();
}
catch (e) {
if ($('#screenblanker').length) { // if #screenblanker exists
$('#screenblanker').slideUp('normal', function() {
$('#screenblanker').remove();
});
} else {
$('body').prepend('
');
$('#screenblanker').slideDown();
}
}
}
var notesMode = false
function toggleNotes()
{
notesMode = !notesMode
if (notesMode) {
$('#notesInfo').show()
debug('notes mode on')
} else {
$('#notesInfo').hide()
}
}
function toggleFollow()
{
mode.follow = ! mode.follow;
if(mode.follow) {
$("#followMode").show().text('Follow Mode:');
getPosition();
} else {
$("#followMode").hide();
}
}
function debug(data)
{
$('#debugInfo').text(data)
}
function toggleKeybinding (setting) {
if (document.onkeydown === null || setting === 'on') {
if (typeof presenterKeyDown === 'function') {
document.onkeydown = presenterKeyDown;
} else {
document.onkeydown = keyDown;
}
} else {
document.onkeydown = null;
}
}
function keyDown(event){
var key = event.keyCode;
debug('keyDown: ' + key);
// avoid overriding browser commands
if (event.ctrlKey || event.altKey || event.metaKey) {
return true;
}
switch(getAction(event)) {
case 'DEBUG': toggleDebug(); break;
case 'PREV': prevStep(); break;
case 'NEXT': nextStep(); break;
case 'REFRESH': reloadSlides(); break;
case 'RELOAD': reloadSlides(true); break;
case 'CONTENTS': toggleContents(); break;
case 'HELP': toggleHelp(); break;
case 'BLANK': blankScreen(); break;
case 'FOOTER': toggleFooter(); break;
case 'FOLLOW': toggleFollow(); break;
case 'NOTES': toggleNotes(); break;
case 'CLEAR': removeResults(); break;
case 'PAUSE': togglePause(); break;
case 'PRESHOW': togglePreShow(); break;
case 'EXECUTE':
debug('executeCode');
executeVisibleCodeBlock();
break;
default:
switch (key) {
case 48: // 0
case 49: // 1
case 50: // 2
case 51: // 3
case 52: // 4
case 53: // 5
case 54: // 6
case 55: // 7
case 56: // 8
case 57: // 9
// concatenate numbers from previous keypress events
gotoSlidenum = gotoSlidenum * 10 + (key - 48);
break;
case 13: // enter/return
// check for a combination of numbers from previous keypress events
if (gotoSlidenum > 0) {
debug('go to ' + gotoSlidenum);
slidenum = gotoSlidenum - 1;
showSlide(true);
gotoSlidenum = 0;
}
break;
default:
break;
}
break;
}
return true;
}
function getAction (event) {
return keymap[getKeyName(event)];
}
function getKeyName (event) {
var keyName = keycode_dictionary[event.keyCode];
if (event.shiftKey && keyName !== undefined) {
// Check for non-alpha characters first, because no idea what toUpperCase will do to those
if (keycode_shifted_keys[keyName] !== undefined) {
keyName = keycode_shifted_keys[keyName];
} else {
keyName = keyName.toUpperCase();
}
}
return keyName;
}
function toggleComplete() {
if($(this).is(':checked')) {
activityIncomplete = false;
sendActivityStatus(true);
if(mode.follow) {
getPosition();
}
}
else {
activityIncomplete = true;
sendActivityStatus(false);
}
}
function toggleDebug () {
debugMode = !debugMode;
doDebugStuff();
}
function reloadSlides (hard) {
if(hard) {
var message = 'Are you sure you want to reload Showoff?';
}
else {
var message = "Are you sure you want to refresh the slide content?\n\n";
message += '(Use `RELOAD` to fully reload the entire UI)';
}
if (confirm(message)) {
loadSlides(loadSlidesBool, loadSlidesPrefix, true, hard);
}
}
function toggleFooter() {
$('#footer').toggle()
}
function toggleHelp () {
var help = $("#help-modal");
help.dialog("isOpen") ? help.dialog("close") : help.dialog("open");
}
function toggleContents () {
$('#feedbackSidebar, #sidebarExit').toggle();
$("#navigation").toggle();
updateMenuChevrons();
}
function swipeLeft() {
nextStep();
}
function swipeRight() {
prevStep();
}
var removeResults = function() {
$('.results').remove();
// if we're a presenter, mirror this on the display window
try { slaveWindow.removeResults() } catch (e) {};
};
var print = function(text) {
removeResults();
var _results = $('
').addClass('results').html('
' + String(text).substring(0, 1500) + '
');
$('body').append(_results);
_results.click(removeResults);
// if we're a presenter, mirror this on the display window
try { slaveWindow.print(text) } catch (e) {};
};
// Execute the first visible executable code block
function executeVisibleCodeBlock()
{
var code = $('code.execute:visible')
if (code.length > 0) {
// make the code block available as $(this) object
executeCode.call(code[0]);
}
}
// determine which code handler to call and execute code sample
function executeCode() {
var codeDiv = $(this);
try {
var lang = codeDiv.attr("class").match(/\blanguage-(\w+)/)[1];
switch(lang) {
case 'javascript':
case 'coffeescript':
executeLocalCode(lang, codeDiv);
break;
default:
executeRemoteCode(lang, codeDiv)
break;
}
}
catch(e) {
debug('No code block to execute: ' + codeDiv.attr('class'));
};
}
// any code that can be run directly in the browser
function executeLocalCode(lang, codeDiv) {
var result = null;
setExecutionSignal(true, codeDiv);
setTimeout(function() { setExecutionSignal(false, codeDiv);}, 1000 );
try {
switch(lang) {
case 'javascript':
result = eval(codeDiv.text());
break;
case 'coffeescript':
result = eval(CoffeeScript.compile(codeDiv.text(), {bare: true}));
break;
default:
result = 'No local exec handler for ' + lang;
}
}
catch(e) {
result = e.message;
};
if (result != null) print(result);
}
// request the server to execute a code block by path and index
function executeRemoteCode(lang, codeDiv) {
var slide = codeDiv.closest('div.content');
var index = slide.find('code.execute').index(codeDiv);
var path = slide.attr('ref');
setExecutionSignal(true, codeDiv);
$.get('/execute/'+lang, {path: path, index: index}, function(result) {
if (result != null) print(result);
setExecutionSignal(false, codeDiv);
});
}
// Provide visual indication that a block of code is running
function setExecutionSignal(status, codeDiv) {
if (status === true) {
codeDiv.addClass("executing");
}
else {
codeDiv.removeClass("executing");
}
// if we're a presenter, mirror this on the display window
try {
var id = codeDiv.closest('div.slide').attr('id');
var index = $('div.slide#'+id+' code.execute').index(codeDiv);
var code = slaveWindow.$('div.slide#'+id+' code.execute').eq(index)
if (status === true) {
code.addClass("executing");
}
else {
code.removeClass("executing");
}
} catch (e) {};
}
/********************
PreShow Code
********************/
var preshow_stop = null;
var preshow_secondsPer = 8;
var preshow_current = 0;
var preshow_images = null;
var preshow_imagesTotal = 0;
var preshow_des = null;
function togglePreShow() {
// The slave window updates this flag, which seems backwards except that the
// slave determines when to finish preshow.
if(preshow_stop) {
try {
slaveWindow.stopPreShow();
}
catch (e) {
stopPreShow();
}
} else {
var seconds = parseFloat(prompt("Minutes from now to start") * 60);
try {
slaveWindow.setupPreShow(seconds);
}
catch (e) {
setupPreShow(seconds);
}
}
}
function setupPreShow(seconds) {
preshow_stop = secondsFromNow(seconds);
try { presenterView.preshow_stop = preshow_stop } catch (e) {}
// footer styling looks icky. Hide it for now.
$('#footer').hide();
$.getJSON("preshow_files", false, function(data) {
$('#preso').after("
");
$.each(data, function(i, n) {
if(n == "preshow.json") {
// has a descriptions file
$.getJSON("/file/_preshow/preshow.json", false, function(data) {
preshow_des = data;
})
} else {
$('#preshow').append('
');
}
})
preshow_images = $('#preshow > img');
preshow_imagesTotal = preshow_images.size();
startPreShow();
});
}
function startPreShow() {
nextPreShowImage();
var nextImage = secondsFromNow(preshow_secondsPer);
var interval = setInterval(function() {
var now = new Date();
if (now > preshow_stop) {
clearInterval(interval);
stopPreShow();
} else {
if (now > nextImage) {
nextImage = secondsFromNow(preshow_secondsPer);
nextPreShowImage();
}
var secondsLeft = Math.floor((preshow_stop.getTime() - now.getTime()) / 1000);
addPreShowTips(secondsLeft);
}
}, 1000)
}
function addPreShowTips(secondsLeft) {
$('#preshow_timer').text('Resuming in: ' + secondsToTime(secondsLeft));
var des = preshow_des && preshow_des[tmpImg.attr("ref")];
if(des) {
$('#tips').show();
$('#tips').text(des);
} else {
$('#tips').hide();
}
}
function secondsFromNow(seconds) {
var now = new Date();
now.setTime(now.getTime() + seconds * 1000);
return now;
}
function secondsToTime(sec) {
var min = Math.floor(sec / 60);
sec = sec - (min * 60);
if(sec < 10) {
sec = "0" + sec;
}
return min + ":" + sec;
}
function stopPreShow() {
try { presenterView.preshow_stop = null } catch (e) {}
preshow_stop = null;
$('#preshow').remove();
$('#tips').remove();
$('#preshow_timer').remove();
loadSlides(loadSlidesBool, loadSlidesPrefix);
}
function nextPreShowImage() {
preshow_current += 1;
if((preshow_current + 1) > preshow_imagesTotal) {
preshow_current = 0;
}
$("#preso").empty();
tmpImg = preshow_images.eq(preshow_current).clone();
$(tmpImg).attr('width', '1020');
$("#preso").html(tmpImg);
}
/********************
End PreShow Code
********************/
function togglePause() {
try {
slaveWindow.togglePause();
}
catch (e) {
$("#pauseScreen").toggle();
}
}
/********************
Stats page
********************/
function setupStats(data)
{
$("#stats div#all div.detail").hide();
$("#stats div#all div.row").click(function() {
$(this).toggleClass('active');
$(this).find("div.detail").slideToggle("fast");
});
['stray', 'idle'].forEach(function(stat){
var percent = data[stat+'_p'];
var selector = '#'+stat;
if(percent > 25) {
$(selector).show();
$(selector+' .label').text(percent+'%');
}
else {
$(selector).hide();
}
});
var location = window.location.pathname == '/presenter' ? '#' : '/#';
var viewers = data['viewers'];
if (viewers) {
if (viewers.length == 1 && viewers[0][3] == 'current') {
$("#viewers").removeClass('zoomline');
$("#viewers").text("All audience members are viewing the presenter's slide.");
}
else {
$("#viewers").zoomline({
max: data['viewmax'],
data: viewers,
click: function(element) { window.location = (location + element.attr("data-left")); }
});
}
}
if (data['elapsed']) {
$("#elapsed").zoomline({
max: data['maxtime'],
data: data['elapsed'],
click: function(element) { window.location = (location + element.attr("data-left")); }
});
}
}
/* Is this a mobile device? */
function mobile() {
return ( $(window).width() <= 640 )
}
/* check browser support for one or more css properties */
function cssPropertySupported(properties) {
properties = typeof(properties) == 'string' ? Array(properties) : properties
var supported = properties.filter(function(property){
return property in document.body.style;
});
return properties.length == supported.length;
}