diff options
Diffstat (limited to 'plugin')
-rw-r--r-- | plugin/notes-server/client.js | 69 | ||||
-rw-r--r-- | plugin/notes-server/index.js | 47 | ||||
-rw-r--r-- | plugin/notes-server/notes.html | 436 |
3 files changed, 392 insertions, 160 deletions
diff --git a/plugin/notes-server/client.js b/plugin/notes-server/client.js index ee60ff7..f7ecfa2 100644 --- a/plugin/notes-server/client.js +++ b/plugin/notes-server/client.js @@ -1,55 +1,28 @@ (function() { + // don't emit events from inside the previews themselves if( window.location.search.match( /receiver/gi ) ) { return; } - var socket = io.connect( window.location.origin ); - var socketId = Math.random().toString().slice( 2 ); + var socket = io.connect( window.location.origin ), + socketId = Math.random().toString().slice( 2 ); console.log( 'View slide notes at ' + window.location.origin + '/notes/' + socketId ); window.open( window.location.origin + '/notes/' + socketId, 'notes-' + socketId ); - // Fires when a fragment is shown - Reveal.addEventListener( 'fragmentshown', function( event ) { - var fragmentData = { - fragment : 'next', - socketId : socketId - }; - socket.emit('fragmentchanged', fragmentData); - } ); + /** + * Posts the current slide data to the notes window + */ + function post() { - // Fires when a fragment is hidden - Reveal.addEventListener( 'fragmenthidden', function( event ) { - var fragmentData = { - fragment : 'previous', - socketId : socketId - }; - socket.emit( 'fragmentchanged', fragmentData ); - } ); - - // Fires when slide is changed - Reveal.addEventListener( 'slidechanged', function( event ) { - var nextindexh, - nextindexv, - slideElement = event.currentSlide, + var slideElement = Reveal.getCurrentSlide(), notesElement = slideElement.querySelector( 'aside.notes' ); - if( slideElement.nextElementSibling && slideElement.parentNode.nodeName == 'SECTION' ) { - nextindexh = event.indexh; - nextindexv = event.indexv + 1; - } else { - nextindexh = event.indexh + 1; - nextindexv = 0; - } - var messageData = { - notes : '', - indexh : event.indexh, - indexv : event.indexv, - nextindexh : nextindexh, - nextindexv : nextindexv, - socketId : socketId, - markdown : false + notes: '', + markdown: false, + socketId: socketId, + state: Reveal.getState() }; // Look for notes defined in a slide attribute @@ -63,6 +36,20 @@ messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string'; } - socket.emit( 'slidechanged', messageData ); - } ); + socket.emit( 'state', messageData ); + + } + + // Monitor events that trigger a change in state + Reveal.addEventListener( 'slidechanged', post ); + Reveal.addEventListener( 'fragmentshown', post ); + Reveal.addEventListener( 'fragmenthidden', post ); + Reveal.addEventListener( 'overviewhidden', post ); + Reveal.addEventListener( 'overviewshown', post ); + Reveal.addEventListener( 'paused', post ); + Reveal.addEventListener( 'resumed', post ); + + // Post the initial state + post(); + }()); diff --git a/plugin/notes-server/index.js b/plugin/notes-server/index.js index 5535c90..b6779d3 100644 --- a/plugin/notes-server/index.js +++ b/plugin/notes-server/index.js @@ -14,46 +14,49 @@ var opts = { baseDir : __dirname + '/../../' }; -io.sockets.on('connection', function(socket) { - socket.on('slidechanged', function(slideData) { - socket.broadcast.emit('slidedata', slideData); - }); - socket.on('fragmentchanged', function(fragmentData) { - socket.broadcast.emit('fragmentdata', fragmentData); +io.sockets.on( 'connection', function( socket ) { + + socket.on( 'state', function( state ) { + socket.broadcast.emit( 'state', state ); }); + }); -app.configure(function() { - [ 'css', 'js', 'images', 'plugin', 'lib' ].forEach(function(dir) { - app.use('/' + dir, staticDir(opts.baseDir + dir)); +app.configure( function() { + + [ 'css', 'js', 'images', 'plugin', 'lib' ].forEach( function( dir ) { + app.use( '/' + dir, staticDir( opts.baseDir + dir ) ); }); + }); -app.get("/", function(req, res) { - res.writeHead(200, {'Content-Type': 'text/html'}); - fs.createReadStream(opts.baseDir + '/index.html').pipe(res); +app.get('/', function( req, res ) { + + res.writeHead( 200, { 'Content-Type': 'text/html' } ); + fs.createReadStream( opts.baseDir + '/index.html' ).pipe( res ); + }); -app.get("/notes/:socketId", function(req, res) { +app.get( '/notes/:socketId', function( req, res ) { - fs.readFile(opts.baseDir + 'plugin/notes-server/notes.html', function(err, data) { - res.send(Mustache.to_html(data.toString(), { + fs.readFile( opts.baseDir + 'plugin/notes-server/notes.html', function( err, data ) { + res.send( Mustache.to_html( data.toString(), { socketId : req.params.socketId })); }); - // fs.createReadStream(opts.baseDir + 'notes-server/notes.html').pipe(res); + }); // Actually listen -app.listen(opts.port || null); +app.listen( opts.port || null ); var brown = '\033[33m', green = '\033[32m', reset = '\033[0m'; -var slidesLocation = "http://localhost" + ( opts.port ? ( ':' + opts.port ) : '' ); +var slidesLocation = 'http://localhost' + ( opts.port ? ( ':' + opts.port ) : '' ); -console.log( brown + "reveal.js - Speaker Notes" + reset ); -console.log( "1. Open the slides at " + green + slidesLocation + reset ); -console.log( "2. Click on the link your JS console to go to the notes page" ); -console.log( "3. Advance through your slides and your notes will advance automatically" ); +console.log( brown + 'reveal.js - Speaker Notes' + reset ); +console.log( '1. Open the slides at ' + green + slidesLocation + reset ); +console.log( '2. Click on the link your JS console to go to the notes page' ); +console.log( '3. Advance through your slides and your notes will advance automatically' ); diff --git a/plugin/notes-server/notes.html b/plugin/notes-server/notes.html index 25d1a62..4ff48f1 100644 --- a/plugin/notes-server/notes.html +++ b/plugin/notes-server/notes.html @@ -3,8 +3,6 @@ <head> <meta charset="utf-8"> - <meta name="viewport" content="width=1150"> - <title>reveal.js - Slide Notes</title> <style> @@ -12,130 +10,374 @@ font-family: Helvetica; } - #notes { - font-size: 24px; - width: 640px; - margin-top: 5px; - clear: left; + #current-slide, + #upcoming-slide, + #speaker-controls { + padding: 6px; + box-sizing: border-box; + -moz-box-sizing: border-box; } - #wrap-current-slide { - width: 640px; - height: 512px; - float: left; - overflow: hidden; + #current-slide iframe, + #upcoming-slide iframe { + width: 100%; + height: 100%; + border: 1px solid #ddd; } - #current-slide { - width: 1280px; - height: 1024px; - border: none; - - -webkit-transform-origin: 0 0; - -moz-transform-origin: 0 0; - -ms-transform-origin: 0 0; - -o-transform-origin: 0 0; - transform-origin: 0 0; - - -webkit-transform: scale(0.5); - -moz-transform: scale(0.5); - -ms-transform: scale(0.5); - -o-transform: scale(0.5); - transform: scale(0.5); - } - - #wrap-next-slide { - width: 448px; - height: 358px; - float: left; - margin: 0 0 0 10px; - overflow: hidden; - } - - #next-slide { - width: 1280px; - height: 1024px; - border: none; - - -webkit-transform-origin: 0 0; - -moz-transform-origin: 0 0; - -ms-transform-origin: 0 0; - -o-transform-origin: 0 0; - transform-origin: 0 0; - - -webkit-transform: scale(0.35); - -moz-transform: scale(0.35); - -ms-transform: scale(0.35); - -o-transform: scale(0.35); - transform: scale(0.35); - } - - .slides { - position: relative; - margin-bottom: 10px; - border: 1px solid black; - border-radius: 2px; - background: rgb(28, 30, 32); - } - - .slides span { + #current-slide .label, + #upcoming-slide .label { position: absolute; - top: 3px; - left: 3px; + top: 10px; + left: 10px; font-weight: bold; font-size: 14px; + z-index: 2; color: rgba( 255, 255, 255, 0.9 ); } + + #current-slide { + position: absolute; + width: 65%; + height: 100%; + top: 0; + left: 0; + padding-right: 0; + } + + #upcoming-slide { + position: absolute; + width: 35%; + height: 40%; + right: 0; + top: 0; + } + + #speaker-controls { + position: absolute; + top: 40%; + right: 0; + width: 35%; + height: 60%; + + font-size: 18px; + } + + .speaker-controls-time.hidden, + .speaker-controls-notes.hidden { + display: none; + } + + .speaker-controls-time .label, + .speaker-controls-notes .label { + text-transform: uppercase; + font-weight: normal; + font-size: 0.66em; + color: #666; + margin: 0; + } + + .speaker-controls-time { + border-bottom: 1px solid rgba( 200, 200, 200, 0.5 ); + margin-bottom: 10px; + padding: 10px 16px; + padding-bottom: 20px; + cursor: pointer; + } + + .speaker-controls-time .reset-button { + opacity: 0; + float: right; + color: #666; + text-decoration: none; + } + .speaker-controls-time:hover .reset-button { + opacity: 1; + } + + .speaker-controls-time .timer, + .speaker-controls-time .clock { + width: 50%; + font-size: 1.9em; + } + + .speaker-controls-time .timer { + float: left; + } + + .speaker-controls-time .clock { + float: right; + text-align: right; + } + + .speaker-controls-time span.mute { + color: #bbb; + } + + .speaker-controls-notes { + padding: 10px 16px; + } + + .speaker-controls-notes .value { + margin-top: 5px; + line-height: 1.4; + font-size: 1.2em; + } + + .clear { + clear: both; + } + + @media screen and (max-width: 1080px) { + #speaker-controls { + font-size: 16px; + } + } + + @media screen and (max-width: 900px) { + #speaker-controls { + font-size: 14px; + } + } + + @media screen and (max-width: 800px) { + #speaker-controls { + font-size: 12px; + } + } + </style> </head> <body> - <div id="wrap-current-slide" class="slides"> - <iframe src="/?receiver" width="1280" height="1024" id="current-slide"></iframe> - </div> + <div id="current-slide"></div> + <div id="upcoming-slide"><span class="label">UPCOMING:</span></div> + <div id="speaker-controls"> + <div class="speaker-controls-time"> + <h4 class="label">Time <span class="reset-button">Click to Reset</span></h4> + <div class="clock"> + <span class="clock-value">0:00 AM</span> + </div> + <div class="timer"> + <span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span> + </div> + <div class="clear"></div> + </div> - <div id="wrap-next-slide" class="slides"> - <iframe src="/?receiver" width="640" height="512" id="next-slide"></iframe> - <span>UPCOMING:</span> + <div class="speaker-controls-notes hidden"> + <h4 class="label">Notes</h4> + <div class="value"></div> + </div> </div> - <div id="notes"></div> <script src="/socket.io/socket.io.js"></script> <script src="/plugin/markdown/marked.js"></script> <script> - var socketId = '{{socketId}}'; - var socket = io.connect(window.location.origin); - var notes = document.getElementById('notes'); - var currentSlide = document.getElementById('current-slide'); - var nextSlide = document.getElementById('next-slide'); + (function() { + + var notes, + notesValue, + currentState, + currentSlide, + upcomingSlide, + connected = false; + + var socket = io.connect( window.location.origin ), + socketId = '{{socketId}}'; + + socket.on( 'state', function( data ) { + + // ignore data from sockets that aren't ours + if( data.socketId !== socketId ) { return; } + + if( connected === false ) { + connected = true; + + setupIframes( data ); + setupKeyboard(); + setupNotes(); + setupTimer(); + + } + + handleStateMessage( data ); - socket.on('slidedata', function(data) { - // ignore data from sockets that aren't ours - if (data.socketId !== socketId) { return; } + } ); + + /** + * Called when the main window sends an updated state. + */ + function handleStateMessage( data ) { + + // Store the most recently set state to avoid circular loops + // applying the same state + currentState = JSON.stringify( data.state ); + + // No need for updating the notes in case of fragment changes + if ( data.notes ) { + notes.classList.remove( 'hidden' ); + if( data.markdown ) { + notesValue.innerHTML = marked( data.notes ); + } + else { + notesValue.innerHTML = data.notes; + } + } + else { + notes.classList.add( 'hidden' ); + } + + // Update the note slides + currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' ); + upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' ); + upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' ); - if (data.markdown) { - notes.innerHTML = marked(data.notes); } - else { - notes.innerHTML = data.notes; + + // Limit to max one state update per X ms + handleStateMessage = debounce( handleStateMessage, 200 ); + + /** + * Forward keyboard events to the current slide window. + * This enables keyboard events to work even if focus + * isn't set on the current slide iframe. + */ + function setupKeyboard() { + + document.addEventListener( 'keydown', function( event ) { + currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' ); + } ); + } - currentSlide.contentWindow.Reveal.slide(data.indexh, data.indexv); - nextSlide.contentWindow.Reveal.slide(data.nextindexh, data.nextindexv); - }); - socket.on('fragmentdata', function(data) { - // ignore data from sockets that aren't ours - if (data.socketId !== socketId) { return; } + /** + * Creates the preview iframes. + */ + function setupIframes( data ) { + + var params = [ + 'receiver', + 'progress=false', + 'history=false', + 'transition=none', + 'backgroundTransition=none' + ].join( '&' ); + + var hash = '#/' + data.state.indexh + '/' + data.state.indexv; + var currentURL = '/?' + params + hash; + var upcomingURL = '/?' + params + '&controls=false' + hash; + + currentSlide = document.createElement( 'iframe' ); + currentSlide.setAttribute( 'width', 1280 ); + currentSlide.setAttribute( 'height', 1024 ); + currentSlide.setAttribute( 'src', currentURL ); + document.querySelector( '#current-slide' ).appendChild( currentSlide ); + + upcomingSlide = document.createElement( 'iframe' ); + upcomingSlide.setAttribute( 'width', 640 ); + upcomingSlide.setAttribute( 'height', 512 ); + upcomingSlide.setAttribute( 'src', upcomingURL ); + document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide ); - if (data.fragment === 'next') { - currentSlide.contentWindow.Reveal.nextFragment(); } - else if (data.fragment === 'previous') { - currentSlide.contentWindow.Reveal.prevFragment(); + + /** + * Setup the notes UI. + */ + function setupNotes() { + + notes = document.querySelector( '.speaker-controls-notes' ); + notesValue = document.querySelector( '.speaker-controls-notes .value' ); + } - }); + + /** + * Create the timer and clock and start updating them + * at an interval. + */ + function setupTimer() { + + var start = new Date(), + timeEl = document.querySelector( '.speaker-controls-time' ), + clockEl = timeEl.querySelector( '.clock-value' ), + hoursEl = timeEl.querySelector( '.hours-value' ), + minutesEl = timeEl.querySelector( '.minutes-value' ), + secondsEl = timeEl.querySelector( '.seconds-value' ); + + function _updateTimer() { + + var diff, hours, minutes, seconds, + now = new Date(); + + diff = now.getTime() - start.getTime(); + hours = Math.floor( diff / ( 1000 * 60 * 60 ) ); + minutes = Math.floor( ( diff / ( 1000 * 60 ) ) % 60 ); + seconds = Math.floor( ( diff / 1000 ) % 60 ); + + clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } ); + hoursEl.innerHTML = zeroPadInteger( hours ); + hoursEl.className = hours > 0 ? '' : 'mute'; + minutesEl.innerHTML = ':' + zeroPadInteger( minutes ); + minutesEl.className = minutes > 0 ? '' : 'mute'; + secondsEl.innerHTML = ':' + zeroPadInteger( seconds ); + + } + + // Update once directly + _updateTimer(); + + // Then update every second + setInterval( _updateTimer, 1000 ); + + timeEl.addEventListener( 'click', function() { + start = new Date(); + _updateTimer(); + return false; + } ); + + } + + function zeroPadInteger( num ) { + + var str = '00' + parseInt( num ); + return str.substring( str.length - 2 ); + + } + + /** + * Limits the frequency at which a function can be called. + */ + function debounce( fn, ms ) { + + var lastTime = 0, + timeout; + + return function() { + + var args = arguments; + var context = this; + + clearTimeout( timeout ); + + var timeSinceLastCall = Date.now() - lastTime; + if( timeSinceLastCall > ms ) { + fn.apply( context, args ); + lastTime = Date.now(); + } + else { + timeout = setTimeout( function() { + fn.apply( context, args ); + lastTime = Date.now(); + }, ms - timeSinceLastCall ); + } + + } + + } + + })(); </script> </body> |