blob: c80e77f13b809e9625868e9a7d9579e0b40cc5ac [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
Bruce's Thinkpad8b73dcf2016-07-14 00:12:43 +0800191 // The overview mode is only useful to the reveal.js instance
192 // where navigation occurs so we don't sync it
193 if( data.state ) delete data.state.overview;
194
JJ Allaireefa6ad42016-01-30 13:12:05 -0500195 // Messages sent by the notes plugin inside of the main window
196 if( data && data.namespace === 'reveal-notes' ) {
197 if( data.type === 'connect' ) {
198 handleConnectMessage( data );
199 }
200 else if( data.type === 'state' ) {
201 handleStateMessage( data );
202 }
203 }
204 // Messages sent by the reveal.js inside of the current slide preview
205 else if( data && data.namespace === 'reveal' ) {
206 if( /ready/.test( data.eventName ) ) {
207 // Send a message back to notify that the handshake is complete
208 window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
209 }
Bruce's Thinkpad8b73dcf2016-07-14 00:12:43 +0800210 else if( /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
211
JJ Allaireefa6ad42016-01-30 13:12:05 -0500212 window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ]} ), '*' );
Bruce's Thinkpad8b73dcf2016-07-14 00:12:43 +0800213
JJ Allaireefa6ad42016-01-30 13:12:05 -0500214 }
215 }
216
217 } );
218
219 /**
220 * Called when the main window is trying to establish a
221 * connection.
222 */
223 function handleConnectMessage( data ) {
224
225 if( connected === false ) {
226 connected = true;
227
228 setupIframes( data );
229 setupKeyboard();
230 setupNotes();
231 setupTimer();
232 }
233
234 }
235
236 /**
237 * Called when the main window sends an updated state.
238 */
239 function handleStateMessage( data ) {
240
241 // Store the most recently set state to avoid circular loops
242 // applying the same state
243 currentState = JSON.stringify( data.state );
244
245 // No need for updating the notes in case of fragment changes
246 if ( data.notes ) {
247 notes.classList.remove( 'hidden' );
248 notesValue.style.whiteSpace = data.whitespace;
249 if( data.markdown ) {
250 notesValue.innerHTML = marked( data.notes );
251 }
252 else {
253 notesValue.innerHTML = data.notes;
254 }
255 }
256 else {
257 notes.classList.add( 'hidden' );
258 }
259
260 // Update the note slides
261 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
262 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
263 upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
264
265 }
266
267 // Limit to max one state update per X ms
268 handleStateMessage = debounce( handleStateMessage, 200 );
269
270 /**
271 * Forward keyboard events to the current slide window.
272 * This enables keyboard events to work even if focus
273 * isn't set on the current slide iframe.
274 */
275 function setupKeyboard() {
276
277 document.addEventListener( 'keydown', function( event ) {
278 currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
279 } );
280
281 }
282
283 /**
284 * Creates the preview iframes.
285 */
286 function setupIframes( data ) {
287
288 var params = [
289 'receiver',
290 'progress=false',
291 'history=false',
292 'transition=none',
293 'autoSlide=0',
294 'backgroundTransition=none'
295 ].join( '&' );
296
Bruce's Thinkpad8b73dcf2016-07-14 00:12:43 +0800297 var urlSeparator = /\?/.test(data.url) ? '&' : '?';
JJ Allaireefa6ad42016-01-30 13:12:05 -0500298 var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
Bruce's Thinkpad8b73dcf2016-07-14 00:12:43 +0800299 var currentURL = data.url + urlSeparator + params + '&postMessageEvents=true' + hash;
300 var upcomingURL = data.url + urlSeparator + params + '&controls=false' + hash;
JJ Allaireefa6ad42016-01-30 13:12:05 -0500301
302 currentSlide = document.createElement( 'iframe' );
303 currentSlide.setAttribute( 'width', 1280 );
304 currentSlide.setAttribute( 'height', 1024 );
305 currentSlide.setAttribute( 'src', currentURL );
306 document.querySelector( '#current-slide' ).appendChild( currentSlide );
307
308 upcomingSlide = document.createElement( 'iframe' );
309 upcomingSlide.setAttribute( 'width', 640 );
310 upcomingSlide.setAttribute( 'height', 512 );
311 upcomingSlide.setAttribute( 'src', upcomingURL );
312 document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
313
314 }
315
316 /**
317 * Setup the notes UI.
318 */
319 function setupNotes() {
320
321 notes = document.querySelector( '.speaker-controls-notes' );
322 notesValue = document.querySelector( '.speaker-controls-notes .value' );
323
324 }
325
326 /**
327 * Create the timer and clock and start updating them
328 * at an interval.
329 */
330 function setupTimer() {
331
332 var start = new Date(),
333 timeEl = document.querySelector( '.speaker-controls-time' ),
334 clockEl = timeEl.querySelector( '.clock-value' ),
335 hoursEl = timeEl.querySelector( '.hours-value' ),
336 minutesEl = timeEl.querySelector( '.minutes-value' ),
337 secondsEl = timeEl.querySelector( '.seconds-value' );
338
339 function _updateTimer() {
340
341 var diff, hours, minutes, seconds,
342 now = new Date();
343
344 diff = now.getTime() - start.getTime();
345 hours = Math.floor( diff / ( 1000 * 60 * 60 ) );
346 minutes = Math.floor( ( diff / ( 1000 * 60 ) ) % 60 );
347 seconds = Math.floor( ( diff / 1000 ) % 60 );
348
349 clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
350 hoursEl.innerHTML = zeroPadInteger( hours );
351 hoursEl.className = hours > 0 ? '' : 'mute';
352 minutesEl.innerHTML = ':' + zeroPadInteger( minutes );
353 minutesEl.className = minutes > 0 ? '' : 'mute';
354 secondsEl.innerHTML = ':' + zeroPadInteger( seconds );
355
356 }
357
358 // Update once directly
359 _updateTimer();
360
361 // Then update every second
362 setInterval( _updateTimer, 1000 );
363
364 timeEl.addEventListener( 'click', function() {
365 start = new Date();
366 _updateTimer();
367 return false;
368 } );
369
370 }
371
372 function zeroPadInteger( num ) {
373
374 var str = '00' + parseInt( num );
375 return str.substring( str.length - 2 );
376
377 }
378
379 /**
380 * Limits the frequency at which a function can be called.
381 */
382 function debounce( fn, ms ) {
383
384 var lastTime = 0,
385 timeout;
386
387 return function() {
388
389 var args = arguments;
390 var context = this;
391
392 clearTimeout( timeout );
393
394 var timeSinceLastCall = Date.now() - lastTime;
395 if( timeSinceLastCall > ms ) {
396 fn.apply( context, args );
397 lastTime = Date.now();
398 }
399 else {
400 timeout = setTimeout( function() {
401 fn.apply( context, args );
402 lastTime = Date.now();
403 }, ms - timeSinceLastCall );
404 }
405
406 }
407
408 }
409
410 })();
411
412 </script>
413 </body>
414</html>