| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| |
| <title>reveal.js - Slide Notes</title> |
| |
| <style> |
| body { |
| font-family: Helvetica; |
| font-size: 18px; |
| } |
| |
| #current-slide, |
| #upcoming-slide, |
| #speaker-controls { |
| padding: 6px; |
| box-sizing: border-box; |
| -moz-box-sizing: border-box; |
| } |
| |
| #current-slide iframe, |
| #upcoming-slide iframe { |
| width: 100%; |
| height: 100%; |
| border: 1px solid #ddd; |
| } |
| |
| #current-slide .label, |
| #upcoming-slide .label { |
| position: absolute; |
| top: 10px; |
| left: 10px; |
| z-index: 2; |
| } |
| |
| .overlay-element { |
| height: 34px; |
| line-height: 34px; |
| padding: 0 10px; |
| text-shadow: none; |
| background: rgba( 220, 220, 220, 0.8 ); |
| color: #222; |
| font-size: 14px; |
| } |
| |
| .overlay-element.interactive:hover { |
| background: rgba( 220, 220, 220, 1 ); |
| } |
| |
| #current-slide { |
| position: absolute; |
| width: 60%; |
| height: 100%; |
| top: 0; |
| left: 0; |
| padding-right: 0; |
| } |
| |
| #upcoming-slide { |
| position: absolute; |
| width: 40%; |
| height: 40%; |
| right: 0; |
| top: 0; |
| } |
| |
| /* Speaker controls */ |
| #speaker-controls { |
| position: absolute; |
| top: 40%; |
| right: 0; |
| width: 40%; |
| height: 60%; |
| overflow: auto; |
| 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; |
| } |
| |
| /* Layout selector */ |
| #speaker-layout { |
| position: absolute; |
| top: 10px; |
| right: 10px; |
| color: #222; |
| z-index: 10; |
| } |
| #speaker-layout select { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| top: 0; |
| left: 0; |
| border: 0; |
| box-shadow: 0; |
| cursor: pointer; |
| opacity: 0; |
| |
| font-size: 1em; |
| background-color: transparent; |
| |
| -moz-appearance: none; |
| -webkit-appearance: none; |
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
| } |
| |
| #speaker-layout select:focus { |
| outline: none; |
| box-shadow: none; |
| } |
| |
| .clear { |
| clear: both; |
| } |
| |
| /* Speaker layout: Wide */ |
| body[data-speaker-layout="wide"] #current-slide, |
| body[data-speaker-layout="wide"] #upcoming-slide { |
| width: 50%; |
| height: 45%; |
| padding: 6px; |
| } |
| |
| body[data-speaker-layout="wide"] #current-slide { |
| top: 0; |
| left: 0; |
| } |
| |
| body[data-speaker-layout="wide"] #upcoming-slide { |
| top: 0; |
| left: 50%; |
| } |
| |
| body[data-speaker-layout="wide"] #speaker-controls { |
| top: 45%; |
| left: 0; |
| width: 100%; |
| height: 50%; |
| font-size: 1.25em; |
| } |
| |
| /* Speaker layout: Tall */ |
| body[data-speaker-layout="tall"] #current-slide, |
| body[data-speaker-layout="tall"] #upcoming-slide { |
| width: 45%; |
| height: 50%; |
| padding: 6px; |
| } |
| |
| body[data-speaker-layout="tall"] #current-slide { |
| top: 0; |
| left: 0; |
| } |
| |
| body[data-speaker-layout="tall"] #upcoming-slide { |
| top: 50%; |
| left: 0; |
| } |
| |
| body[data-speaker-layout="tall"] #speaker-controls { |
| padding-top: 40px; |
| top: 0; |
| left: 45%; |
| width: 55%; |
| height: 100%; |
| font-size: 1.25em; |
| } |
| |
| /* Speaker layout: Notes only */ |
| body[data-speaker-layout="notes-only"] #current-slide, |
| body[data-speaker-layout="notes-only"] #upcoming-slide { |
| display: none; |
| } |
| |
| body[data-speaker-layout="notes-only"] #speaker-controls { |
| padding-top: 40px; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| font-size: 1.25em; |
| } |
| |
| </style> |
| </head> |
| |
| <body> |
| |
| <div id="current-slide"></div> |
| <div id="upcoming-slide"><span class="overlay-element 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 class="speaker-controls-notes hidden"> |
| <h4 class="label">Notes</h4> |
| <div class="value"></div> |
| </div> |
| </div> |
| <div id="speaker-layout" class="overlay-element interactive"> |
| <span class="speaker-layout-label"></span> |
| <select class="speaker-layout-dropdown"></select> |
| </div> |
| |
| <script src="/socket.io/socket.io.js"></script> |
| <script src="/plugin/markdown/marked.js"></script> |
| |
| <script> |
| (function() { |
| |
| var notes, |
| notesValue, |
| currentState, |
| currentSlide, |
| upcomingSlide, |
| layoutLabel, |
| layoutDropdown, |
| connected = false; |
| |
| var socket = io.connect( window.location.origin ), |
| socketId = '{{socketId}}'; |
| |
| var SPEAKER_LAYOUTS = { |
| 'default': 'Default', |
| 'wide': 'Wide', |
| 'tall': 'Tall', |
| 'notes-only': 'Notes only' |
| }; |
| |
| socket.on( 'statechanged', function( data ) { |
| |
| // ignore data from sockets that aren't ours |
| if( data.socketId !== socketId ) { return; } |
| |
| if( connected === false ) { |
| connected = true; |
| |
| setupKeyboard(); |
| setupNotes(); |
| setupTimer(); |
| |
| } |
| |
| handleStateMessage( data ); |
| |
| } ); |
| |
| setupLayout(); |
| |
| // Load our presentation iframes |
| setupIframes(); |
| |
| // Once the iframes have loaded, emit a signal saying there's |
| // a new subscriber which will trigger a 'statechanged' |
| // message to be sent back |
| window.addEventListener( 'message', function( event ) { |
| |
| var data = JSON.parse( event.data ); |
| |
| if( data && data.namespace === 'reveal' ) { |
| if( /ready/.test( data.eventName ) ) { |
| socket.emit( 'new-subscriber', { socketId: socketId } ); |
| } |
| } |
| |
| // Messages sent by reveal.js inside of the current slide preview |
| if( data && data.namespace === 'reveal' ) { |
| if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) { |
| socket.emit( 'statechanged-speaker', { state: data.state } ); |
| } |
| } |
| |
| } ); |
| |
| /** |
| * 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' }), '*' ); |
| |
| } |
| |
| // 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 ] }), '*' ); |
| } ); |
| |
| } |
| |
| /** |
| * Creates the preview iframes. |
| */ |
| function setupIframes() { |
| |
| var params = [ |
| 'receiver', |
| 'progress=false', |
| 'history=false', |
| 'transition=none', |
| 'backgroundTransition=none' |
| ].join( '&' ); |
| |
| var currentURL = '/?' + params + '&postMessageEvents=true'; |
| var upcomingURL = '/?' + params + '&controls=false'; |
| |
| 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 ); |
| |
| } |
| |
| /** |
| * 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; |
| } ); |
| |
| } |
| |
| /** |
| * Sets up the speaker view layout and layout selector. |
| */ |
| function setupLayout() { |
| |
| layoutDropdown = document.querySelector( '.speaker-layout-dropdown' ); |
| layoutLabel = document.querySelector( '.speaker-layout-label' ); |
| |
| // Render the list of available layouts |
| for( var id in SPEAKER_LAYOUTS ) { |
| var option = document.createElement( 'option' ); |
| option.setAttribute( 'value', id ); |
| option.textContent = SPEAKER_LAYOUTS[ id ]; |
| layoutDropdown.appendChild( option ); |
| } |
| |
| // Monitor the dropdown for changes |
| layoutDropdown.addEventListener( 'change', function( event ) { |
| |
| setLayout( layoutDropdown.value ); |
| |
| }, false ); |
| |
| // Restore any currently persisted layout |
| setLayout( getLayout() ); |
| |
| } |
| |
| /** |
| * Sets a new speaker view layout. The layout is persisted |
| * in local storage. |
| */ |
| function setLayout( value ) { |
| |
| var title = SPEAKER_LAYOUTS[ value ]; |
| |
| layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' ); |
| layoutDropdown.value = value; |
| |
| document.body.setAttribute( 'data-speaker-layout', value ); |
| |
| // Persist locally |
| if( window.localStorage ) { |
| window.localStorage.setItem( 'reveal-speaker-layout', value ); |
| } |
| |
| } |
| |
| /** |
| * Returns the ID of the most recently set speaker layout |
| * or our default layout if none has been set. |
| */ |
| function getLayout() { |
| |
| if( window.localStorage ) { |
| var layout = window.localStorage.getItem( 'reveal-speaker-layout' ); |
| if( layout ) { |
| return layout; |
| } |
| } |
| |
| // Default to the first record in the layouts hash |
| for( var id in SPEAKER_LAYOUTS ) { |
| return id; |
| } |
| |
| } |
| |
| 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> |
| </html> |