diff options
author | jackycute | 2015-11-28 21:57:35 -0600 |
---|---|---|
committer | jackycute | 2015-11-28 21:57:35 -0600 |
commit | d6dec55a07ceacbc21aa211b90c18fcf0d189013 (patch) | |
tree | b6f43191518b16f74ca33b1406567a0ffcaa6f32 /public/plugin/notes | |
parent | 75ae505a1544d8999e6705aad7ed96ff5a6eb0e5 (diff) | |
parent | f51b7370f030c5cd1235fa7a23514a7e1ccef2dd (diff) |
Merge pull request #43 from x000032001/master
Add Slide Mode
Diffstat (limited to 'public/plugin/notes')
-rw-r--r-- | public/plugin/notes/notes.html | 406 | ||||
-rw-r--r-- | public/plugin/notes/notes.js | 122 |
2 files changed, 528 insertions, 0 deletions
diff --git a/public/plugin/notes/notes.html b/public/plugin/notes/notes.html new file mode 100644 index 00000000..0cc8cf61 --- /dev/null +++ b/public/plugin/notes/notes.html @@ -0,0 +1,406 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + + <title>reveal.js - Slide Notes</title> + + <style> + body { + font-family: Helvetica; + } + + #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; + 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%; + 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; + } + + .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="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 class="speaker-controls-notes hidden"> + <h4 class="label">Notes</h4> + <div class="value"></div> + </div> + </div> + + <script src="../../plugin/markdown/marked.js"></script> + <script> + + (function() { + + var notes, + notesValue, + currentState, + currentSlide, + upcomingSlide, + connected = false; + + window.addEventListener( 'message', function( event ) { + + var data = JSON.parse( event.data ); + + // Messages sent by the notes plugin inside of the main window + if( data && data.namespace === 'reveal-notes' ) { + if( data.type === 'connect' ) { + handleConnectMessage( data ); + } + else if( data.type === 'state' ) { + handleStateMessage( data ); + } + } + // Messages sent by the reveal.js inside of the current slide preview + else if( data && data.namespace === 'reveal' ) { + if( /ready/.test( data.eventName ) ) { + // Send a message back to notify that the handshake is complete + window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' ); + } + else if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) { + window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ]} ), '*' ); + } + } + + } ); + + /** + * Called when the main window is trying to establish a + * connection. + */ + function handleConnectMessage( data ) { + + if( connected === false ) { + connected = true; + + setupIframes( data ); + setupKeyboard(); + setupNotes(); + setupTimer(); + } + + } + + /** + * 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( data ) { + + var params = [ + 'receiver', + 'progress=false', + 'history=false', + 'transition=none', + 'autoSlide=0', + 'backgroundTransition=none' + ].join( '&' ); + + var hash = '#/' + data.state.indexh + '/' + data.state.indexv; + var currentURL = data.url + '?' + params + '&postMessageEvents=true' + hash; + var upcomingURL = data.url + '?' + 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 ); + + } + + /** + * 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> +</html> diff --git a/public/plugin/notes/notes.js b/public/plugin/notes/notes.js new file mode 100644 index 00000000..e42329d2 --- /dev/null +++ b/public/plugin/notes/notes.js @@ -0,0 +1,122 @@ +/** + * Handles opening of and synchronization with the reveal.js + * notes window. + * + * Handshake process: + * 1. This window posts 'connect' to notes window + * - Includes URL of presentation to show + * 2. Notes window responds with 'connected' when it is available + * 3. This window proceeds to send the current presentation state + * to the notes window + */ +var RevealNotes = (function() { + + function openNotes() { + var jsFileLocation = document.querySelector('script[src$="notes.js"]').src; // this js file path + jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, ''); // the js folder path + var notesPopup = window.open( jsFileLocation + 'notes.html', 'reveal.js - Notes', 'width=1100,height=700' ); + + /** + * Connect to the notes window through a postmessage handshake. + * Using postmessage enables us to work in situations where the + * origins differ, such as a presentation being opened from the + * file system. + */ + function connect() { + // Keep trying to connect until we get a 'connected' message back + var connectInterval = setInterval( function() { + notesPopup.postMessage( JSON.stringify( { + namespace: 'reveal-notes', + type: 'connect', + url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search, + state: Reveal.getState() + } ), '*' ); + }, 500 ); + + window.addEventListener( 'message', function( event ) { + var data = JSON.parse( event.data ); + if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { + clearInterval( connectInterval ); + onConnected(); + } + } ); + } + + /** + * Posts the current slide data to the notes window + */ + function post() { + + var slideElement = Reveal.getCurrentSlide(), + notesElement = slideElement.querySelector( 'aside.notes' ); + + var messageData = { + namespace: 'reveal-notes', + type: 'state', + notes: '', + markdown: false, + state: Reveal.getState() + }; + + // Look for notes defined in a slide attribute + if( slideElement.hasAttribute( 'data-notes' ) ) { + messageData.notes = slideElement.getAttribute( 'data-notes' ); + } + + // Look for notes defined in an aside element + if( notesElement ) { + messageData.notes = notesElement.innerHTML; + messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string'; + } + + notesPopup.postMessage( JSON.stringify( messageData ), '*' ); + + } + + /** + * Called once we have established a connection to the notes + * window. + */ + function onConnected() { + + // 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(); + + } + + connect(); + } + + if( !/receiver/i.test( window.location.search ) ) { + + // If the there's a 'notes' query set, open directly + if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { + openNotes(); + } + + // Open the notes when the 's' key is hit + document.addEventListener( 'keydown', function( event ) { + // Disregard the event if the target is editable or a + // modifier is present + if ( document.querySelector( ':focus' ) !== null || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; + + if( event.keyCode === 83 ) { + event.preventDefault(); + openNotes(); + } + }, false ); + + } + + return { open: openNotes }; + +})(); |