diff options
Diffstat (limited to 'presentation/plugin/notes-server/notes.html')
-rw-r--r-- | presentation/plugin/notes-server/notes.html | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/presentation/plugin/notes-server/notes.html b/presentation/plugin/notes-server/notes.html new file mode 100644 index 0000000..7a42256 --- /dev/null +++ b/presentation/plugin/notes-server/notes.html @@ -0,0 +1,590 @@ +<!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 class="overlay-element interactive" id="speaker-layout"> + <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> |