1<!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				font-size: 18px;
12			}
13
14			#current-slide,
15			#upcoming-slide,
16			#speaker-controls {
17				padding: 6px;
18				box-sizing: border-box;
19				-moz-box-sizing: border-box;
20			}
21
22			#current-slide iframe,
23			#upcoming-slide iframe {
24				width: 100%;
25				height: 100%;
26				border: 1px solid #ddd;
27			}
28
29			#current-slide .label,
30			#upcoming-slide .label {
31				position: absolute;
32				top: 10px;
33				left: 10px;
34				z-index: 2;
35			}
36
37			.overlay-element {
38				height: 34px;
39				line-height: 34px;
40				padding: 0 10px;
41				text-shadow: none;
42				background: rgba( 220, 220, 220, 0.8 );
43				color: #222;
44				font-size: 14px;
45			}
46
47			.overlay-element.interactive:hover {
48				background: rgba( 220, 220, 220, 1 );
49			}
50
51			#current-slide {
52				position: absolute;
53				width: 60%;
54				height: 100%;
55				top: 0;
56				left: 0;
57				padding-right: 0;
58			}
59
60			#upcoming-slide {
61				position: absolute;
62				width: 40%;
63				height: 40%;
64				right: 0;
65				top: 0;
66			}
67
68			/* Speaker controls */
69			#speaker-controls {
70				position: absolute;
71				top: 40%;
72				right: 0;
73				width: 40%;
74				height: 60%;
75				overflow: auto;
76				font-size: 18px;
77			}
78
79				.speaker-controls-time.hidden,
80				.speaker-controls-notes.hidden {
81					display: none;
82				}
83
84				.speaker-controls-time .label,
85				.speaker-controls-notes .label {
86					text-transform: uppercase;
87					font-weight: normal;
88					font-size: 0.66em;
89					color: #666;
90					margin: 0;
91				}
92
93				.speaker-controls-time {
94					border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
95					margin-bottom: 10px;
96					padding: 10px 16px;
97					padding-bottom: 20px;
98					cursor: pointer;
99				}
100
101				.speaker-controls-time .reset-button {
102					opacity: 0;
103					float: right;
104					color: #666;
105					text-decoration: none;
106				}
107				.speaker-controls-time:hover .reset-button {
108					opacity: 1;
109				}
110
111				.speaker-controls-time .timer,
112				.speaker-controls-time .clock {
113					width: 50%;
114					font-size: 1.9em;
115				}
116
117				.speaker-controls-time .timer {
118					float: left;
119				}
120
121				.speaker-controls-time .clock {
122					float: right;
123					text-align: right;
124				}
125
126				.speaker-controls-time span.mute {
127					color: #bbb;
128				}
129
130				.speaker-controls-notes {
131					padding: 10px 16px;
132				}
133
134				.speaker-controls-notes .value {
135					margin-top: 5px;
136					line-height: 1.4;
137					font-size: 1.2em;
138				}
139
140			/* Layout selector */
141			#speaker-layout {
142				position: absolute;
143				top: 10px;
144				right: 10px;
145				color: #222;
146				z-index: 10;
147			}
148				#speaker-layout select {
149					position: absolute;
150					width: 100%;
151					height: 100%;
152					top: 0;
153					left: 0;
154					border: 0;
155					box-shadow: 0;
156					cursor: pointer;
157					opacity: 0;
158
159					font-size: 1em;
160					background-color: transparent;
161
162					-moz-appearance: none;
163					-webkit-appearance: none;
164					-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
165				}
166
167				#speaker-layout select:focus {
168					outline: none;
169					box-shadow: none;
170				}
171
172			.clear {
173				clear: both;
174			}
175
176			/* Speaker layout: Wide */
177			body[data-speaker-layout="wide"] #current-slide,
178			body[data-speaker-layout="wide"] #upcoming-slide {
179				width: 50%;
180				height: 45%;
181				padding: 6px;
182			}
183
184			body[data-speaker-layout="wide"] #current-slide {
185				top: 0;
186				left: 0;
187			}
188
189			body[data-speaker-layout="wide"] #upcoming-slide {
190				top: 0;
191				left: 50%;
192			}
193
194			body[data-speaker-layout="wide"] #speaker-controls {
195				top: 45%;
196				left: 0;
197				width: 100%;
198				height: 50%;
199				font-size: 1.25em;
200			}
201
202			/* Speaker layout: Tall */
203			body[data-speaker-layout="tall"] #current-slide,
204			body[data-speaker-layout="tall"] #upcoming-slide {
205				width: 45%;
206				height: 50%;
207				padding: 6px;
208			}
209
210			body[data-speaker-layout="tall"] #current-slide {
211				top: 0;
212				left: 0;
213			}
214
215			body[data-speaker-layout="tall"] #upcoming-slide {
216				top: 50%;
217				left: 0;
218			}
219
220			body[data-speaker-layout="tall"] #speaker-controls {
221				padding-top: 40px;
222				top: 0;
223				left: 45%;
224				width: 55%;
225				height: 100%;
226				font-size: 1.25em;
227			}
228
229			/* Speaker layout: Notes only */
230			body[data-speaker-layout="notes-only"] #current-slide,
231			body[data-speaker-layout="notes-only"] #upcoming-slide {
232				display: none;
233			}
234
235			body[data-speaker-layout="notes-only"] #speaker-controls {
236				padding-top: 40px;
237				top: 0;
238				left: 0;
239				width: 100%;
240				height: 100%;
241				font-size: 1.25em;
242			}
243
244		</style>
245	</head>
246
247	<body>
248
249		<div id="current-slide"></div>
250		<div id="upcoming-slide"><span class="overlay-element label">Upcoming</span></div>
251		<div id="speaker-controls">
252			<div class="speaker-controls-time">
253				<h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
254				<div class="clock">
255					<span class="clock-value">0:00 AM</span>
256				</div>
257				<div class="timer">
258					<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
259				</div>
260				<div class="clear"></div>
261			</div>
262
263			<div class="speaker-controls-notes hidden">
264				<h4 class="label">Notes</h4>
265				<div class="value"></div>
266			</div>
267		</div>
268		<div id="speaker-layout" class="overlay-element interactive">
269			<span class="speaker-layout-label"></span>
270			<select class="speaker-layout-dropdown"></select>
271		</div>
272
273		<script src="/socket.io/socket.io.js"></script>
274		<script src="/plugin/markdown/marked.js"></script>
275
276		<script>
277		(function() {
278
279			var notes,
280				notesValue,
281				currentState,
282				currentSlide,
283				upcomingSlide,
284				layoutLabel,
285				layoutDropdown,
286				connected = false;
287
288			var socket = io.connect( window.location.origin ),
289				socketId = '{{socketId}}';
290
291			var SPEAKER_LAYOUTS = {
292				'default': 'Default',
293				'wide': 'Wide',
294				'tall': 'Tall',
295				'notes-only': 'Notes only'
296			};
297
298			socket.on( 'statechanged', function( data ) {
299
300				// ignore data from sockets that aren't ours
301				if( data.socketId !== socketId ) { return; }
302
303				if( connected === false ) {
304					connected = true;
305
306					setupKeyboard();
307					setupNotes();
308					setupTimer();
309
310				}
311
312				handleStateMessage( data );
313
314			} );
315
316			setupLayout();
317
318			// Load our presentation iframes
319			setupIframes();
320
321			// Once the iframes have loaded, emit a signal saying there's
322			// a new subscriber which will trigger a 'statechanged'
323			// message to be sent back
324			window.addEventListener( 'message', function( event ) {
325
326				var data = JSON.parse( event.data );
327
328				if( data && data.namespace === 'reveal' ) {
329					if( /ready/.test( data.eventName ) ) {
330						socket.emit( 'new-subscriber', { socketId: socketId } );
331					}
332				}
333
334				// Messages sent by reveal.js inside of the current slide preview
335				if( data && data.namespace === 'reveal' ) {
336					if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
337						socket.emit( 'statechanged-speaker', { state: data.state } );
338					}
339				}
340
341			} );
342
343			/**
344			 * Called when the main window sends an updated state.
345			 */
346			function handleStateMessage( data ) {
347
348				// Store the most recently set state to avoid circular loops
349				// applying the same state
350				currentState = JSON.stringify( data.state );
351
352				// No need for updating the notes in case of fragment changes
353				if ( data.notes ) {
354					notes.classList.remove( 'hidden' );
355					if( data.markdown ) {
356						notesValue.innerHTML = marked( data.notes );
357					}
358					else {
359						notesValue.innerHTML = data.notes;
360					}
361				}
362				else {
363					notes.classList.add( 'hidden' );
364				}
365
366				// Update the note slides
367				currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
368				upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
369				upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
370
371			}
372
373			// Limit to max one state update per X ms
374			handleStateMessage = debounce( handleStateMessage, 200 );
375
376			/**
377			 * Forward keyboard events to the current slide window.
378			 * This enables keyboard events to work even if focus
379			 * isn't set on the current slide iframe.
380			 */
381			function setupKeyboard() {
382
383				document.addEventListener( 'keydown', function( event ) {
384					currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
385				} );
386
387			}
388
389			/**
390			 * Creates the preview iframes.
391			 */
392			function setupIframes() {
393
394				var params = [
395					'receiver',
396					'progress=false',
397					'history=false',
398					'transition=none',
399					'backgroundTransition=none'
400				].join( '&' );
401
402				var currentURL = '/?' + params + '&postMessageEvents=true';
403				var upcomingURL = '/?' + params + '&controls=false';
404
405				currentSlide = document.createElement( 'iframe' );
406				currentSlide.setAttribute( 'width', 1280 );
407				currentSlide.setAttribute( 'height', 1024 );
408				currentSlide.setAttribute( 'src', currentURL );
409				document.querySelector( '#current-slide' ).appendChild( currentSlide );
410
411				upcomingSlide = document.createElement( 'iframe' );
412				upcomingSlide.setAttribute( 'width', 640 );
413				upcomingSlide.setAttribute( 'height', 512 );
414				upcomingSlide.setAttribute( 'src', upcomingURL );
415				document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
416
417			}
418
419			/**
420			 * Setup the notes UI.
421			 */
422			function setupNotes() {
423
424				notes = document.querySelector( '.speaker-controls-notes' );
425				notesValue = document.querySelector( '.speaker-controls-notes .value' );
426
427			}
428
429			/**
430			 * Create the timer and clock and start updating them
431			 * at an interval.
432			 */
433			function setupTimer() {
434
435				var start = new Date(),
436					timeEl = document.querySelector( '.speaker-controls-time' ),
437					clockEl = timeEl.querySelector( '.clock-value' ),
438					hoursEl = timeEl.querySelector( '.hours-value' ),
439					minutesEl = timeEl.querySelector( '.minutes-value' ),
440					secondsEl = timeEl.querySelector( '.seconds-value' );
441
442				function _updateTimer() {
443
444					var diff, hours, minutes, seconds,
445						now = new Date();
446
447					diff = now.getTime() - start.getTime();
448					hours = Math.floor( diff / ( 1000 * 60 * 60 ) );
449					minutes = Math.floor( ( diff / ( 1000 * 60 ) ) % 60 );
450					seconds = Math.floor( ( diff / 1000 ) % 60 );
451
452					clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
453					hoursEl.innerHTML = zeroPadInteger( hours );
454					hoursEl.className = hours > 0 ? '' : 'mute';
455					minutesEl.innerHTML = ':' + zeroPadInteger( minutes );
456					minutesEl.className = minutes > 0 ? '' : 'mute';
457					secondsEl.innerHTML = ':' + zeroPadInteger( seconds );
458
459				}
460
461				// Update once directly
462				_updateTimer();
463
464				// Then update every second
465				setInterval( _updateTimer, 1000 );
466
467				timeEl.addEventListener( 'click', function() {
468					start = new Date();
469					_updateTimer();
470					return false;
471				} );
472
473			}
474
475			/**
476				 * Sets up the speaker view layout and layout selector.
477				 */
478				function setupLayout() {
479
480					layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
481					layoutLabel = document.querySelector( '.speaker-layout-label' );
482
483					// Render the list of available layouts
484					for( var id in SPEAKER_LAYOUTS ) {
485						var option = document.createElement( 'option' );
486						option.setAttribute( 'value', id );
487						option.textContent = SPEAKER_LAYOUTS[ id ];
488						layoutDropdown.appendChild( option );
489					}
490
491					// Monitor the dropdown for changes
492					layoutDropdown.addEventListener( 'change', function( event ) {
493
494						setLayout( layoutDropdown.value );
495
496					}, false );
497
498					// Restore any currently persisted layout
499					setLayout( getLayout() );
500
501				}
502
503				/**
504				 * Sets a new speaker view layout. The layout is persisted
505				 * in local storage.
506				 */
507				function setLayout( value ) {
508
509					var title = SPEAKER_LAYOUTS[ value ];
510
511					layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
512					layoutDropdown.value = value;
513
514					document.body.setAttribute( 'data-speaker-layout', value );
515
516					// Persist locally
517					if( window.localStorage ) {
518						window.localStorage.setItem( 'reveal-speaker-layout', value );
519					}
520
521				}
522
523				/**
524				 * Returns the ID of the most recently set speaker layout
525				 * or our default layout if none has been set.
526				 */
527				function getLayout() {
528
529					if( window.localStorage ) {
530						var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
531						if( layout ) {
532							return layout;
533						}
534					}
535
536					// Default to the first record in the layouts hash
537					for( var id in SPEAKER_LAYOUTS ) {
538						return id;
539					}
540
541				}
542
543			function zeroPadInteger( num ) {
544
545				var str = '00' + parseInt( num );
546				return str.substring( str.length - 2 );
547
548			}
549
550			/**
551			 * Limits the frequency at which a function can be called.
552			 */
553			function debounce( fn, ms ) {
554
555				var lastTime = 0,
556					timeout;
557
558				return function() {
559
560					var args = arguments;
561					var context = this;
562
563					clearTimeout( timeout );
564
565					var timeSinceLastCall = Date.now() - lastTime;
566					if( timeSinceLastCall > ms ) {
567						fn.apply( context, args );
568						lastTime = Date.now();
569					}
570					else {
571						timeout = setTimeout( function() {
572							fn.apply( context, args );
573							lastTime = Date.now();
574						}, ms - timeSinceLastCall );
575					}
576
577				}
578
579			}
580
581		})();
582		</script>
583
584	</body>
585</html>
586