blob: 75f1b9b0318ecfc3f1b04121c0f2c2bc6db1c398 [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 overflow: auto;
63
64 font-size: 18px;
65 }
66
67 .speaker-controls-time.hidden,
68 .speaker-controls-notes.hidden {
69 display: none;
70 }
71
72 .speaker-controls-time .label,
73 .speaker-controls-notes .label {
74 text-transform: uppercase;
75 font-weight: normal;
76 font-size: 0.66em;
77 color: #666;
78 margin: 0;
79 }
80
81 .speaker-controls-time {
82 border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
83 margin-bottom: 10px;
84 padding: 10px 16px;
85 padding-bottom: 20px;
86 cursor: pointer;
87 }
88
89 .speaker-controls-time .reset-button {
90 opacity: 0;
91 float: right;
92 color: #666;
93 text-decoration: none;
94 }
95 .speaker-controls-time:hover .reset-button {
96 opacity: 1;
97 }
98
99 .speaker-controls-time .timer,
100 .speaker-controls-time .clock {
101 width: 50%;
102 font-size: 1.9em;
103 }
104
105 .speaker-controls-time .timer {
106 float: left;
107 }
108
109 .speaker-controls-time .clock {
110 float: right;
111 text-align: right;
112 }
113
114 .speaker-controls-time span.mute {
115 color: #bbb;
116 }
117
118 .speaker-controls-notes {
119 padding: 10px 16px;
120 }
121
122 .speaker-controls-notes .value {
123 margin-top: 5px;
124 line-height: 1.4;
125 font-size: 1.2em;
126 }
127
128 .clear {
129 clear: both;
130 }
131
132 @media screen and (max-width: 1080px) {
133 #speaker-controls {
134 font-size: 16px;
135 }
136 }
137
138 @media screen and (max-width: 900px) {
139 #speaker-controls {
140 font-size: 14px;
141 }
142 }
143
144 @media screen and (max-width: 800px) {
145 #speaker-controls {
146 font-size: 12px;
147 }
148 }
149
150 </style>
151 </head>
152
153 <body>
154
155 <div id="current-slide"></div>
156 <div id="upcoming-slide"><span class="label">UPCOMING:</span></div>
157 <div id="speaker-controls">
158 <div class="speaker-controls-time">
159 <h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
160 <div class="clock">
161 <span class="clock-value">0:00 AM</span>
162 </div>
163 <div class="timer">
164 <span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
165 </div>
166 <div class="clear"></div>
167 </div>
168
169 <div class="speaker-controls-notes hidden">
170 <h4 class="label">Notes</h4>
171 <div class="value"></div>
172 </div>
173 </div>
174
175 <script src="../../plugin/markdown/marked.js"></script>
176 <script>
177
178 (function() {
179
180 var notes,
181 notesValue,
182 currentState,
183 currentSlide,
184 upcomingSlide,
185 connected = false;
186
187 window.addEventListener( 'message', function( event ) {
188
189 var data = JSON.parse( event.data );
190
191 // Messages sent by the notes plugin inside of the main window
192 if( data && data.namespace === 'reveal-notes' ) {
193 if( data.type === 'connect' ) {
194 handleConnectMessage( data );
195 }
196 else if( data.type === 'state' ) {
197 handleStateMessage( data );
198 }
199 }
200 // Messages sent by the reveal.js inside of the current slide preview
201 else if( data && data.namespace === 'reveal' ) {
202 if( /ready/.test( data.eventName ) ) {
203 // Send a message back to notify that the handshake is complete
204 window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
205 }
206 else if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
207 window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ]} ), '*' );
208 }
209 }
210
211 } );
212
213 /**
214 * Called when the main window is trying to establish a
215 * connection.
216 */
217 function handleConnectMessage( data ) {
218
219 if( connected === false ) {
220 connected = true;
221
222 setupIframes( data );
223 setupKeyboard();
224 setupNotes();
225 setupTimer();
226 }
227
228 }
229
230 /**
231 * Called when the main window sends an updated state.
232 */
233 function handleStateMessage( data ) {
234
235 // Store the most recently set state to avoid circular loops
236 // applying the same state
237 currentState = JSON.stringify( data.state );
238
239 // No need for updating the notes in case of fragment changes
240 if ( data.notes ) {
241 notes.classList.remove( 'hidden' );
242 notesValue.style.whiteSpace = data.whitespace;
243 if( data.markdown ) {
244 notesValue.innerHTML = marked( data.notes );
245 }
246 else {
247 notesValue.innerHTML = data.notes;
248 }
249 }
250 else {
251 notes.classList.add( 'hidden' );
252 }
253
254 // Update the note slides
255 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
256 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
257 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
258
259 }
260
261 // Limit to max one state update per X ms
262 handleStateMessage = debounce( handleStateMessage, 200 );
263
264 /**
265 * Forward keyboard events to the current slide window.
266 * This enables keyboard events to work even if focus
267 * isn't set on the current slide iframe.
268 */
269 function setupKeyboard() {
270
271 document.addEventListener( 'keydown', function( event ) {
272 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
273 } );
274
275 }
276
277 /**
278 * Creates the preview iframes.
279 */
280 function setupIframes( data ) {
281
282 var params = [
283 'receiver',
284 'progress=false',
285 'history=false',
286 'transition=none',
287 'autoSlide=0',
288 'backgroundTransition=none'
289 ].join( '&' );
290
291 var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
292 var currentURL = data.url + '?' + params + '&postMessageEvents=true' + hash;
293 var upcomingURL = data.url + '?' + params + '&controls=false' + hash;
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
405 </script>
406 </body>
407</html>