// presenter js var slaveWindow = null; var nextWindow = null; var notesWindow = null; var paceData = []; section = 'notes'; // which section the presenter has chosen to view $(document).ready(function(){ // set up the presenter modes mode = { track: true, follow: true, update: true, slave: false, next: false, notes: false}; // attempt to open another window for the presentation if the mode defaults // to enabling this. It does not by default, so this is likely a no-op. openSlave(); // the presenter window doesn't need the reload on resize bit $(window).unbind('resize'); $("#startTimer").click(function() { startTimer() }); $("#pauseTimer").click(function() { toggleTimer() }); $("#stopTimer").click(function() { stopTimer() }); /* zoom slide to match preview size, then set up resize handler. */ zoom(); $(window).resize(function() { zoom(); }); $('#statslink').click(function(e) { presenterPopupToggle('/stats', e); }); $('#downloadslink').click(function(e) { presenterPopupToggle('/download', e); }); // Bind events for mobile viewing if( mobile() ) { $('#preso').unbind('tap').unbind('swipeleft').unbind('swiperight'); $('#preso').addSwipeEvents(). bind('tap', presNextStep). // next bind('swipeleft', presNextStep). // next bind('swiperight', presPrevStep); // prev $('#topbar #slideSource').click( function(e) { $('#sidebar').toggle(); }); $('#topbar #update').click( function(e) { e.preventDefault(); $.get("/getpage", function(data) { gotoSlide(data); }); }); } $('#remoteToggle').change( toggleFollower ); $('#followerToggle').change( toggleUpdater ); setInterval(function() { updatePace() }, 1000); // Tell the showoff server that we're a presenter register(); }); function presenterPopupToggle(page, event) { event.preventDefault(); var popup = $('#presenterPopup'); if (popup.length > 0) { popup.slideUp(200, function () { popup.remove(); }); } else { popup = $('
'); popup.attr('id', 'presenterPopup'); $.get(page, function(data) { var link = $(''), content = $('
'); link.attr({ href: page, target: '_new' }); link.text('Open in a new page...'); content.attr('id', page.substring(1, page.length)); content.append(link); /* use .sibliings() because of how jquery formats $(data) */ content.append($(data).siblings('#wrapper').html()); popup.append(content); setupStats(); // this function is in showoff.js because /stats does not load presenter.js $('body').append(popup); popup.slideDown(200); // #presenterPopup is display: none by default }); } } function reportIssue() { var slide = $("span#slideFile").text(); var link = issueUrl + encodeURIComponent('Issue with slide: ' + slide); window.open(link); } // open browser to remote edit URL function editSlide() { var slide = $("span#slideFile").text().replace(/:\d+$/, ''); var link = editUrl + slide + ".md"; window.open(link); } // call the edit endpoint to open up a local file editor function openEditor() { var slide = $("span#slideFile").text().replace(/:\d+$/, ''); var link = '/edit/' + slide + ".md"; $.get(link); } function toggleSlave() { mode.slave = !mode.slave; openSlave(); } function openSlave() { if (mode.slave) { try { if(slaveWindow == null || typeof(slaveWindow) == 'undefined' || slaveWindow.closed){ slaveWindow = window.open('/' + window.location.hash, 'toolbar'); } else if(slaveWindow.location.hash != window.location.hash) { // maybe we need to reset content? slaveWindow.location.href = '/' + window.location.hash; } // maintain the pointer back to the parent. slaveWindow.presenterView = window; slaveWindow.mode = { track: false, slave: true, follow: false }; // Add a class to differentiate from the audience view slaveWindow.document.getElementById("preso").className = 'display'; $('#slaveWindow').addClass('enabled'); } catch(e) { console.log('Failed to open or connect display window. Popup blocker?'); } // Set up a maintenance loop to keep the connection between windows. I wish there were a cleaner way to do this. if (typeof maintainSlave == 'undefined') { maintainSlave = setInterval(openSlave, 1000); } } else { try { slaveWindow && slaveWindow.close(); $('#slaveWindow').removeClass('enabled'); } catch (e) { console.log('Display window failed to close properly.'); } } } function nextSlideNum(url) { // Some fudging because the first slide is slide[0] but numbered 1 in the URL console.log(typeof(url)); var snum; if (typeof(url) == 'undefined') { snum = currentSlideFromParams()+1; } else { snum = currentSlideFromParams()+2; } return snum; } function toggleNext() { mode.next = !mode.next; openNext(); } function openNext() { if (mode.next) { try { if(nextWindow == null || typeof(nextWindow) == 'undefined' || nextWindow.closed){ nextWindow = window.open('/?track=false&feedback=false&next=true#' + nextSlideNum(true),'','width=320,height=300'); } else if(nextWindow.location.hash != '#' + nextSlideNum(true)) { // maybe we need to reset content? nextWindow.location.href = '/?track=false&feedback=false&next=true#' + nextSlideNum(true); } // maintain the pointer back to the parent. nextWindow.presenterView = window; nextWindow.mode = { track: false, next: true, follow: true }; $('#nextWindow').addClass('enabled'); } catch(e) { console.log('Failed to open or connect next window. Popup blocker?'); } } else { try { nextWindow && nextWindow.close(); $('#nextWindow').removeClass('enabled'); } catch (e) { console.log('Next window failed to close properly.'); } } } function toggleNotes() { mode.notes = !mode.notes; openNotes(); } function openNotes() { if (mode.notes) { try { if(notesWindow == null || typeof(notesWindow) == 'undefined' || notesWindow.closed){ notesWindow = window.open('', '', 'width=350,height=450'); postSlide(); } $('#notesWindow').addClass('enabled'); } catch(e) { console.log('Failed to open notes window. Popup blocker?'); } } else { try { notesWindow && notesWindow.close(); $('#notesWindow').removeClass('enabled'); } catch (e) { console.log('Notes window failed to close properly.'); } } } function printSlides() { try { var printWindow = window.open('/print'); printWindow.window.print(); } catch(e) { console.log('Failed to open print window. Popup blocker?'); } } function postQuestion(question, questionID) { var questionItem = $('
  • ').text(question).attr('id', questionID); questionItem.click( function(e) { markCompleted($(this).attr('id')); removeQuestion(questionID); }); $("#unanswered").append(questionItem); updateQuestionIndicator(); } function removeQuestion(questionID) { var question = $("li#"+questionID); question.toggleClass('answered') .remove(); $('#answered').append($(question)); updateQuestionIndicator(); } function updateQuestionIndicator() { try { slaveWindow.updateQuestionIndicator( $('#unanswered li').length ) } catch (e) {} } function paceFeedback(pace) { var now = new Date(); switch(pace) { case 'faster': paceData.push({time: now, pace: -1}); break; // too fast case 'slower': paceData.push({time: now, pace: 1}); break; // too slow } updatePace(); } function updatePace() { // pace notices expire in a few minutes var cutoff = 3 * 60 * 1000; var expiration = new Date().getTime() - cutoff; var scale = 10; // this should max out around 5 clicks in either direction var sum = 50; // start in the middle // Loops through and calculates a decaying average for (var index = 0; index < paceData.length; index++) { var notice = paceData[index]; if(notice.time < expiration) { paceData.splice( index, 1 ); } else { var ratio = (notice.time - expiration) / cutoff; sum += (notice.pace * scale * ratio); } } var position = Math.max(Math.min(sum, 90), 10); // between 10 and 90 $("#paceMarker").css({ left: position+"%" }); if(position > 75) { $("#paceFast").show(); } else { $("#paceFast").hide(); } if(position < 25) { $("#paceSlow").show(); } else { $("#paceSlow").hide(); } } // extend this function to add presenter bits var origGotoSlide = gotoSlide; gotoSlide = function (slideNum) { origGotoSlide.call(this, slideNum) try { slaveWindow.gotoSlide(slideNum, false) } catch (e) {} if ( !mobile() ) { $("#navigation li li").get(slidenum).scrollIntoView(); } postSlide() } // override with an alternate implementation. // We need to do this before opening the websocket because the socket only // inherits cookies present at initialization time. reconnectControlChannel = function() { $.ajax({ url: "presenter", success: function() { // In jQuery 1.4.2, this branch seems to be taken unconditionally. It doesn't // matter though, as the disconnected() callback routes back here anyway. console.log("Refreshing presenter cookie"); connectControlChannel(); }, error: function() { console.log("Showoff server unavailable"); setTimeout(reconnectControlChannel(), 5000); }, }); } function markCompleted(questionID) { ws.send(JSON.stringify({ message: 'complete', questionID: questionID})); } function update() { if(mode.update) { var slideName = $("#slideFile").text(); ws.send(JSON.stringify({ message: 'update', slide: slidenum, name: slideName})); } } // Tell the showoff server that we're a presenter, giving the socket time to initialize function register() { setTimeout( function() { try { ws.send(JSON.stringify({ message: 'register' })); } catch(e) { console.log("Registration failed. Sleeping"); // try again, until the socket finally lets us register register(); } }, 5000); } function presPrevStep() { prevStep(); try { slaveWindow.prevStep(false) } catch (e) {}; try { nextWindow.gotoSlide(nextSlideNum()) } catch (e) {}; postSlide(); update(); } function presNextStep() { nextStep(); try { slaveWindow.nextStep(false) } catch (e) {}; try { nextWindow.gotoSlide(nextSlideNum()) } catch (e) {}; postSlide(); update(); } function postSlide() { if(currentSlide) { // clear out any existing rendered forms try { clearInterval(renderFormInterval) } catch(e) { } $('#notes div.form').empty(); 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); var sections = getCurrentSections(); if(sections.size() > 1) { var ul = $('