blob: ad8c71908b3ad98ab822aa49ae18301518f8995f [file] [log] [blame]
JJ Allaireefa6ad42016-01-30 13:12:05 -05001<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5
6 <title>reveal.js - Slide Notes</title>
7
8 <style>
9 body {
10 font-family: Helvetica;
11 }
12
13 #current-slide,
14 #upcoming-slide,
15 #speaker-controls {
16 padding: 6px;
17 box-sizing: border-box;
18 -moz-box-sizing: border-box;
19 }
20
21 #current-slide iframe,
22 #upcoming-slide iframe {
23 width: 100%;
24 height: 100%;
25 border: 1px solid #ddd;
26 }
27
28 #current-slide .label,
29 #upcoming-slide .label {
30 position: absolute;
31 top: 10px;
32 left: 10px;
33 font-weight: bold;
34 font-size: 14px;
35 z-index: 2;
36 color: rgba( 255, 255, 255, 0.9 );
37 }
38
39 #current-slide {
40 position: absolute;
41 width: 65%;
42 height: 100%;
43 top: 0;
44 left: 0;
45 padding-right: 0;
46 }
47
48 #upcoming-slide {
49 position: absolute;
50 width: 35%;
51 height: 40%;
52 right: 0;
53 top: 0;
54 }
55
56 #speaker-controls {
57 position: absolute;
58 top: 40%;
59 right: 0;
60 width: 35%;
61 height: 60%;
62
63 font-size: 18px;
64 }
65
66 .speaker-controls-time.hidden,
67 .speaker-controls-notes.hidden {
68 display: none;
69 }
70
71 .speaker-controls-time .label,
72 .speaker-controls-notes .label {
73 text-transform: uppercase;
74 font-weight: normal;
75 font-size: 0.66em;
76 color: #666;
77 margin: 0;
78 }
79
80 .speaker-controls-time {
81 border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
82 margin-bottom: 10px;
83 padding: 10px 16px;
84 padding-bottom: 20px;
85 cursor: pointer;
86 }
87
88 .speaker-controls-time .reset-button {
89 opacity: 0;
90 float: right;
91 color: #666;
92 text-decoration: none;
93 }
94 .speaker-controls-time:hover .reset-button {
95 opacity: 1;
96 }
97
98 .speaker-controls-time .timer,
99 .speaker-controls-time .clock {
100 width: 50%;
101 font-size: 1.9em;
102 }
103
104 .speaker-controls-time .timer {
105 float: left;
106 }
107
108 .speaker-controls-time .clock {
109 float: right;
110 text-align: right;
111 }
112
113 .speaker-controls-time span.mute {
114 color: #bbb;
115 }
116
117 .speaker-controls-notes {
118 padding: 10px 16px;
119 }
120
121 .speaker-controls-notes .value {
122 margin-top: 5px;
123 line-height: 1.4;
124 font-size: 1.2em;
125 }
126
127 .clear {
128 clear: both;
129 }
130
131 @media screen and (max-width: 1080px) {
132 #speaker-controls {
133 font-size: 16px;
134 }
135 }
136
137 @media screen and (max-width: 900px) {
138 #speaker-controls {
139 font-size: 14px;
140 }
141 }
142
143 @media screen and (max-width: 800px) {
144 #speaker-controls {
145 font-size: 12px;
146 }
147 }
148
149 </style>
150 </head>
151
152 <body>
153
154 <div id="current-slide"></div>
155 <div id="upcoming-slide"><span class="label">UPCOMING:</span></div>
156 <div id="speaker-controls">
157 <div class="speaker-controls-time">
158 <h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
159 <div class="clock">
160 <span class="clock-value">0:00 AM</span>
161 </div>
162 <div class="timer">
163 <span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
164 </div>
165 <div class="clear"></div>
166 </div>
167
168 <div class="speaker-controls-notes hidden">
169 <h4 class="label">Notes</h4>
170 <div class="value"></div>
171 </div>
172 </div>
173
174 <script src="/socket.io/socket.io.js"></script>
175 <script src="/plugin/markdown/marked.js"></script>
176
177 <script>
178 (function() {
179
180 var notes,
181 notesValue,
182 currentState,
183 currentSlide,
184 upcomingSlide,
185 connected = false;
186
187 var socket = io.connect( window.location.origin ),
188 socketId = '{{socketId}}';
189
190 socket.on( 'statechanged', function( data ) {
191
192 // ignore data from sockets that aren't ours
193 if( data.socketId !== socketId ) { return; }
194
195 if( connected === false ) {
196 connected = true;
197
198 setupKeyboard();
199 setupNotes();
200 setupTimer();
201
202 }
203
204 handleStateMessage( data );
205
206 } );
207
208 // Load our presentation iframes
209 setupIframes();
210
211 // Once the iframes have loaded, emit a signal saying there's
212 // a new subscriber which will trigger a 'statechanged'
213 // message to be sent back
214 window.addEventListener( 'message', function( event ) {
215
216 var data = JSON.parse( event.data );
217
218 if( data && data.namespace === 'reveal' ) {
219 if( /ready/.test( data.eventName ) ) {
220 socket.emit( 'new-subscriber', { socketId: socketId } );
221 }
222 }
223
224 // Messages sent by reveal.js inside of the current slide preview
225 if( data && data.namespace === 'reveal' ) {
226 if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
227 socket.emit( 'statechanged-speaker', { state: data.state } );
228 }
229 }
230
231 } );
232
233 /**
234 * Called when the main window sends an updated state.
235 */
236 function handleStateMessage( data ) {
237
238 // Store the most recently set state to avoid circular loops
239 // applying the same state
240 currentState = JSON.stringify( data.state );
241
242 // No need for updating the notes in case of fragment changes
243 if ( data.notes ) {
244 notes.classList.remove( 'hidden' );
245 if( data.markdown ) {
246 notesValue.innerHTML = marked( data.notes );
247 }
248 else {
249 notesValue.innerHTML = data.notes;
250 }
251 }
252 else {
253 notes.classList.add( 'hidden' );
254 }
255
256 // Update the note slides
257 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
258 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
259 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
260
261 }
262
263 // Limit to max one state update per X ms
264 handleStateMessage = debounce( handleStateMessage, 200 );
265
266 /**
267 * Forward keyboard events to the current slide window.
268 * This enables keyboard events to work even if focus
269 * isn't set on the current slide iframe.
270 */
271 function setupKeyboard() {
272
273 document.addEventListener( 'keydown', function( event ) {
274 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
275 } );
276
277 }
278
279 /**
280 * Creates the preview iframes.
281 */
282 function setupIframes() {
283
284 var params = [
285 'receiver',
286 'progress=false',
287 'history=false',
288 'transition=none',
289 'backgroundTransition=none'
290 ].join( '&' );
291
292 var currentURL = '/?' + params + '&postMessageEvents=true';
293 var upcomingURL = '/?' + params + '&controls=false';
294
295 currentSlide = document.createElement( 'iframe' );
296 currentSlide.setAttribute( 'width', 1280 );
297 currentSlide.setAttribute( 'height', 1024 );
298 currentSlide.setAttribute( 'src', currentURL );
299 document.querySelector( '#current-slide' ).appendChild( currentSlide );
300
301 upcomingSlide = document.createElement( 'iframe' );
302 upcomingSlide.setAttribute( 'width', 640 );
303 upcomingSlide.setAttribute( 'height', 512 );
304 upcomingSlide.setAttribute( 'src', upcomingURL );
305 document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
306
307 }
308
309 /**
310 * Setup the notes UI.
311 */
312 function setupNotes() {
313
314 notes = document.querySelector( '.speaker-controls-notes' );
315 notesValue = document.querySelector( '.speaker-controls-notes .value' );
316
317 }
318
319 /**
320 * Create the timer and clock and start updating them
321 * at an interval.
322 */
323 function setupTimer() {
324
325 var start = new Date(),
326 timeEl = document.querySelector( '.speaker-controls-time' ),
327 clockEl = timeEl.querySelector( '.clock-value' ),
328 hoursEl = timeEl.querySelector( '.hours-value' ),
329 minutesEl = timeEl.querySelector( '.minutes-value' ),
330 secondsEl = timeEl.querySelector( '.seconds-value' );
331
332 function _updateTimer() {
333
334 var diff, hours, minutes, seconds,
335 now = new Date();
336
337 diff = now.getTime() - start.getTime();
338 hours = Math.floor( diff / ( 1000 * 60 * 60 ) );
339 minutes = Math.floor( ( diff / ( 1000 * 60 ) ) % 60 );
340 seconds = Math.floor( ( diff / 1000 ) % 60 );
341
342 clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
343 hoursEl.innerHTML = zeroPadInteger( hours );
344 hoursEl.className = hours > 0 ? '' : 'mute';
345 minutesEl.innerHTML = ':' + zeroPadInteger( minutes );
346 minutesEl.className = minutes > 0 ? '' : 'mute';
347 secondsEl.innerHTML = ':' + zeroPadInteger( seconds );
348
349 }
350
351 // Update once directly
352 _updateTimer();
353
354 // Then update every second
355 setInterval( _updateTimer, 1000 );
356
357 timeEl.addEventListener( 'click', function() {
358 start = new Date();
359 _updateTimer();
360 return false;
361 } );
362
363 }
364
365 function zeroPadInteger( num ) {
366
367 var str = '00' + parseInt( num );
368 return str.substring( str.length - 2 );
369
370 }
371
372 /**
373 * Limits the frequency at which a function can be called.
374 */
375 function debounce( fn, ms ) {
376
377 var lastTime = 0,
378 timeout;
379
380 return function() {
381
382 var args = arguments;
383 var context = this;
384
385 clearTimeout( timeout );
386
387 var timeSinceLastCall = Date.now() - lastTime;
388 if( timeSinceLastCall > ms ) {
389 fn.apply( context, args );
390 lastTime = Date.now();
391 }
392 else {
393 timeout = setTimeout( function() {
394 fn.apply( context, args );
395 lastTime = Date.now();
396 }, ms - timeSinceLastCall );
397 }
398
399 }
400
401 }
402
403 })();
404 </script>
405
406 </body>
407</html>