diff options
Diffstat (limited to 'presentation/plugin/notes/notes.html')
-rw-r--r-- | presentation/plugin/notes/notes.html | 829 |
1 files changed, 829 insertions, 0 deletions
diff --git a/presentation/plugin/notes/notes.html b/presentation/plugin/notes/notes.html new file mode 100644 index 0000000..2b8452f --- /dev/null +++ b/presentation/plugin/notes/notes.html @@ -0,0 +1,829 @@ +<!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; + } + + #connection-status { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 20; + padding: 30% 20% 20% 20%; + font-size: 18px; + color: #222; + background: #fff; + text-align: center; + box-sizing: border-box; + line-height: 1.4; + } + + .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-pace .label, + .speaker-controls-notes .label { + text-transform: uppercase; + font-weight: normal; + font-size: 0.66em; + color: #666; + margin: 0; + } + + .speaker-controls-time, .speaker-controls-pace { + 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%; + } + + .speaker-controls-time .timer, + .speaker-controls-time .clock, + .speaker-controls-time .pacing .hours-value, + .speaker-controls-time .pacing .minutes-value, + .speaker-controls-time .pacing .seconds-value { + font-size: 1.9em; + } + + .speaker-controls-time .timer { + float: left; + } + + .speaker-controls-time .clock { + float: right; + text-align: right; + } + + .speaker-controls-time span.mute { + opacity: 0.3; + } + + .speaker-controls-time .pacing-title { + margin-top: 5px; + } + + .speaker-controls-time .pacing.ahead { + color: blue; + } + + .speaker-controls-time .pacing.on-track { + color: green; + } + + .speaker-controls-time .pacing.behind { + color: red; + } + + .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; + } + + @media screen and (max-width: 1080px) { + body[data-speaker-layout="default"] #speaker-controls { + font-size: 16px; + } + } + + @media screen and (max-width: 900px) { + body[data-speaker-layout="default"] #speaker-controls { + font-size: 14px; + } + } + + @media screen and (max-width: 800px) { + body[data-speaker-layout="default"] #speaker-controls { + font-size: 12px; + } + } + + </style> +</head> + +<body> + +<div id="connection-status">Loading speaker view...</div> + +<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> + + <h4 class="label pacing-title" style="display: none">Pacing – Time to finish current slide</h4> + <div class="pacing" style="display: none"> + <span class="hours-value">00</span><span class="minutes-value">:00</span><span + class="seconds-value">:00</span> + </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="../../plugin/markdown/marked.js"></script> +<script> + + (function () { + + var notes, + notesValue, + currentState, + currentSlide, + upcomingSlide, + layoutLabel, + layoutDropdown, + pendingCalls = {}, + lastRevealApiCallId = 0, + connected = false; + + var SPEAKER_LAYOUTS = { + 'default': 'Default', + 'wide': 'Wide', + 'tall': 'Tall', + 'notes-only': 'Notes only' + }; + + setupLayout(); + + var connectionStatus = document.querySelector('#connection-status'); + var connectionTimeout = setTimeout(function () { + connectionStatus.innerHTML = 'Error connecting to main window.<br>Please try closing and reopening the speaker view.'; + }, 5000); + + window.addEventListener('message', function (event) { + + clearTimeout(connectionTimeout); + connectionStatus.style.display = 'none'; + + var data = JSON.parse(event.data); + + // The overview mode is only useful to the reveal.js instance + // where navigation occurs so we don't sync it + if (data.state) delete data.state.overview; + + // 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); + } else if (data.type === 'return') { + pendingCalls[data.callId](data.result); + delete pendingCalls[data.callId]; + } + } + // 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|paused|resumed/.test(data.eventName) && currentState !== JSON.stringify(data.state)) { + + window.opener.postMessage(JSON.stringify({method: 'setState', args: [data.state]}), '*'); + + } + } + + }); + + /** + * Asynchronously calls the Reveal.js API of the main frame. + */ + function callRevealApi(methodName, methodArguments, callback) { + + var callId = ++lastRevealApiCallId; + pendingCalls[callId] = callback; + window.opener.postMessage(JSON.stringify({ + namespace: 'reveal-notes', + type: 'call', + callId: callId, + methodName: methodName, + arguments: methodArguments + }), '*'); + + } + + /** + * 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'); + notesValue.style.whiteSpace = data.whitespace; + 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. + * + * Block F5 default handling, it reloads and disconnects + * the speaker notes window. + */ + function setupKeyboard() { + + document.addEventListener('keydown', function (event) { + if (event.keyCode === 116 || (event.metaKey && event.keyCode === 82)) { + event.preventDefault(); + return false; + } + 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 urlSeparator = /\?/.test(data.url) ? '&' : '?'; + var hash = '#/' + data.state.indexh + '/' + data.state.indexv; + var currentURL = data.url + urlSeparator + params + '&postMessageEvents=true' + hash; + var upcomingURL = data.url + urlSeparator + 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'); + + } + + function getTimings(callback) { + + callRevealApi('getSlidesAttributes', [], function (slideAttributes) { + callRevealApi('getConfig', [], function (config) { + var defaultTiming = config.defaultTiming; + if (defaultTiming == null) { + callback(null); + return; + } + + var timings = []; + for (var i in slideAttributes) { + var slide = slideAttributes[i]; + var timing = defaultTiming; + if (slide.hasOwnProperty('data-timing')) { + var t = slide['data-timing']; + timing = parseInt(t); + if (isNaN(timing)) { + console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming); + timing = defaultTiming; + } + } + timings.push(timing); + } + + callback(timings); + }); + }); + + } + + /** + * Return the number of seconds allocated for presenting + * all slides up to and including this one. + */ + function getTimeAllocated(timings, callback) { + + callRevealApi('getSlidePastCount', [], function (currentSlide) { + var allocated = 0; + for (var i in timings.slice(0, currentSlide + 1)) { + allocated += timings[i]; + } + callback(allocated); + }); + + } + + /** + * 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'), + pacingTitleEl = timeEl.querySelector('.pacing-title'), + pacingEl = timeEl.querySelector('.pacing'), + pacingHoursEl = pacingEl.querySelector('.hours-value'), + pacingMinutesEl = pacingEl.querySelector('.minutes-value'), + pacingSecondsEl = pacingEl.querySelector('.seconds-value'); + + var timings = null; + getTimings(function (_timings) { + + timings = _timings; + if (_timings !== null) { + pacingTitleEl.style.removeProperty('display'); + pacingEl.style.removeProperty('display'); + } + + // Update once directly + _updateTimer(); + + // Then update every second + setInterval(_updateTimer, 1000); + + }); + + + function _resetTimer() { + + if (timings == null) { + start = new Date(); + _updateTimer(); + } else { + // Reset timer to beginning of current slide + getTimeAllocated(timings, function (slideEndTimingSeconds) { + var slideEndTiming = slideEndTimingSeconds * 1000; + callRevealApi('getSlidePastCount', [], function (currentSlide) { + var currentSlideTiming = timings[currentSlide] * 1000; + var previousSlidesTiming = slideEndTiming - currentSlideTiming; + var now = new Date(); + start = new Date(now.getTime() - previousSlidesTiming); + _updateTimer(); + }); + }); + } + + } + + timeEl.addEventListener('click', function () { + _resetTimer(); + return false; + }); + + function _displayTime(hrEl, minEl, secEl, time) { + + var sign = Math.sign(time) == -1 ? "-" : ""; + time = Math.abs(Math.round(time / 1000)); + var seconds = time % 60; + var minutes = Math.floor(time / 60) % 60; + var hours = Math.floor(time / (60 * 60)); + hrEl.innerHTML = sign + zeroPadInteger(hours); + if (hours == 0) { + hrEl.classList.add('mute'); + } else { + hrEl.classList.remove('mute'); + } + minEl.innerHTML = ':' + zeroPadInteger(minutes); + if (hours == 0 && minutes == 0) { + minEl.classList.add('mute'); + } else { + minEl.classList.remove('mute'); + } + secEl.innerHTML = ':' + zeroPadInteger(seconds); + } + + function _updateTimer() { + + var diff, hours, minutes, seconds, + now = new Date(); + + diff = now.getTime() - start.getTime(); + + clockEl.innerHTML = now.toLocaleTimeString('en-US', {hour12: true, hour: '2-digit', minute: '2-digit'}); + _displayTime(hoursEl, minutesEl, secondsEl, diff); + if (timings !== null) { + _updatePacing(diff); + } + + } + + function _updatePacing(diff) { + + getTimeAllocated(timings, function (slideEndTimingSeconds) { + var slideEndTiming = slideEndTimingSeconds * 1000; + + callRevealApi('getSlidePastCount', [], function (currentSlide) { + var currentSlideTiming = timings[currentSlide] * 1000; + var timeLeftCurrentSlide = slideEndTiming - diff; + if (timeLeftCurrentSlide < 0) { + pacingEl.className = 'pacing behind'; + } else if (timeLeftCurrentSlide < currentSlideTiming) { + pacingEl.className = 'pacing on-track'; + } else { + pacingEl.className = 'pacing ahead'; + } + _displayTime(pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide); + }); + }); + } + + } + + /** + * 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 (supportsLocalStorage()) { + 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 (supportsLocalStorage()) { + 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 supportsLocalStorage() { + + try { + localStorage.setItem('test', 'test'); + localStorage.removeItem('test'); + return true; + } catch (e) { + 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> |