all repos — slides @ dd23301e4567c63669a7d53d084ef399ac52f436

Reveal-md slides I made for various occasions

réalise/_static/plugin/notes/speaker-view.html (view raw)

  1<!--
  2	NOTE: You need to build the notes plugin after making changes to this file.
  3-->
  4<html lang="en">
  5	<head>
  6		<meta charset="utf-8">
  7
  8		<title>reveal.js - Speaker View</title>
  9
 10		<style>
 11			body {
 12				font-family: Helvetica;
 13				font-size: 18px;
 14			}
 15
 16			#current-slide,
 17			#upcoming-slide,
 18			#speaker-controls {
 19				padding: 6px;
 20				box-sizing: border-box;
 21				-moz-box-sizing: border-box;
 22			}
 23
 24			#current-slide iframe,
 25			#upcoming-slide iframe {
 26				width: 100%;
 27				height: 100%;
 28				border: 1px solid #ddd;
 29			}
 30
 31			#current-slide .label,
 32			#upcoming-slide .label {
 33				position: absolute;
 34				top: 10px;
 35				left: 10px;
 36				z-index: 2;
 37			}
 38
 39			#connection-status {
 40				position: absolute;
 41				top: 0;
 42				left: 0;
 43				width: 100%;
 44				height: 100%;
 45				z-index: 20;
 46				padding: 30% 20% 20% 20%;
 47				font-size: 18px;
 48				color: #222;
 49				background: #fff;
 50				text-align: center;
 51				box-sizing: border-box;
 52				line-height: 1.4;
 53			}
 54
 55			.overlay-element {
 56				height: 34px;
 57				line-height: 34px;
 58				padding: 0 10px;
 59				text-shadow: none;
 60				background: rgba( 220, 220, 220, 0.8 );
 61				color: #222;
 62				font-size: 14px;
 63			}
 64
 65			.overlay-element.interactive:hover {
 66				background: rgba( 220, 220, 220, 1 );
 67			}
 68
 69			#current-slide {
 70				position: absolute;
 71				width: 60%;
 72				height: 100%;
 73				top: 0;
 74				left: 0;
 75				padding-right: 0;
 76			}
 77
 78			#upcoming-slide {
 79				position: absolute;
 80				width: 40%;
 81				height: 40%;
 82				right: 0;
 83				top: 0;
 84			}
 85
 86			/* Speaker controls */
 87			#speaker-controls {
 88				position: absolute;
 89				top: 40%;
 90				right: 0;
 91				width: 40%;
 92				height: 60%;
 93				overflow: auto;
 94				font-size: 18px;
 95			}
 96
 97				.speaker-controls-time.hidden,
 98				.speaker-controls-notes.hidden {
 99					display: none;
100				}
101
102				.speaker-controls-time .label,
103				.speaker-controls-pace .label,
104				.speaker-controls-notes .label {
105					text-transform: uppercase;
106					font-weight: normal;
107					font-size: 0.66em;
108					color: #666;
109					margin: 0;
110				}
111
112				.speaker-controls-time, .speaker-controls-pace {
113					border-bottom: 1px solid rgba( 200, 200, 200, 0.5 );
114					margin-bottom: 10px;
115					padding: 10px 16px;
116					padding-bottom: 20px;
117					cursor: pointer;
118				}
119
120				.speaker-controls-time .reset-button {
121					opacity: 0;
122					float: right;
123					color: #666;
124					text-decoration: none;
125				}
126				.speaker-controls-time:hover .reset-button {
127					opacity: 1;
128				}
129
130				.speaker-controls-time .timer,
131				.speaker-controls-time .clock {
132					width: 50%;
133				}
134
135				.speaker-controls-time .timer,
136				.speaker-controls-time .clock,
137				.speaker-controls-time .pacing .hours-value,
138				.speaker-controls-time .pacing .minutes-value,
139				.speaker-controls-time .pacing .seconds-value {
140					font-size: 1.9em;
141				}
142
143				.speaker-controls-time .timer {
144					float: left;
145				}
146
147				.speaker-controls-time .clock {
148					float: right;
149					text-align: right;
150				}
151
152				.speaker-controls-time span.mute {
153					opacity: 0.3;
154				}
155
156				.speaker-controls-time .pacing-title {
157					margin-top: 5px;
158				}
159
160				.speaker-controls-time .pacing.ahead {
161					color: blue;
162				}
163
164				.speaker-controls-time .pacing.on-track {
165					color: green;
166				}
167
168				.speaker-controls-time .pacing.behind {
169					color: red;
170				}
171
172				.speaker-controls-notes {
173					padding: 10px 16px;
174				}
175
176				.speaker-controls-notes .value {
177					margin-top: 5px;
178					line-height: 1.4;
179					font-size: 1.2em;
180				}
181
182			/* Layout selector */
183			#speaker-layout {
184				position: absolute;
185				top: 10px;
186				right: 10px;
187				color: #222;
188				z-index: 10;
189			}
190				#speaker-layout select {
191					position: absolute;
192					width: 100%;
193					height: 100%;
194					top: 0;
195					left: 0;
196					border: 0;
197					box-shadow: 0;
198					cursor: pointer;
199					opacity: 0;
200
201					font-size: 1em;
202					background-color: transparent;
203
204					-moz-appearance: none;
205					-webkit-appearance: none;
206					-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
207				}
208
209				#speaker-layout select:focus {
210					outline: none;
211					box-shadow: none;
212				}
213
214			.clear {
215				clear: both;
216			}
217
218			/* Speaker layout: Wide */
219			body[data-speaker-layout="wide"] #current-slide,
220			body[data-speaker-layout="wide"] #upcoming-slide {
221				width: 50%;
222				height: 45%;
223				padding: 6px;
224			}
225
226			body[data-speaker-layout="wide"] #current-slide {
227				top: 0;
228				left: 0;
229			}
230
231			body[data-speaker-layout="wide"] #upcoming-slide {
232				top: 0;
233				left: 50%;
234			}
235
236			body[data-speaker-layout="wide"] #speaker-controls {
237				top: 45%;
238				left: 0;
239				width: 100%;
240				height: 50%;
241				font-size: 1.25em;
242			}
243
244			/* Speaker layout: Tall */
245			body[data-speaker-layout="tall"] #current-slide,
246			body[data-speaker-layout="tall"] #upcoming-slide {
247				width: 45%;
248				height: 50%;
249				padding: 6px;
250			}
251
252			body[data-speaker-layout="tall"] #current-slide {
253				top: 0;
254				left: 0;
255			}
256
257			body[data-speaker-layout="tall"] #upcoming-slide {
258				top: 50%;
259				left: 0;
260			}
261
262			body[data-speaker-layout="tall"] #speaker-controls {
263				padding-top: 40px;
264				top: 0;
265				left: 45%;
266				width: 55%;
267				height: 100%;
268				font-size: 1.25em;
269			}
270
271			/* Speaker layout: Notes only */
272			body[data-speaker-layout="notes-only"] #current-slide,
273			body[data-speaker-layout="notes-only"] #upcoming-slide {
274				display: none;
275			}
276
277			body[data-speaker-layout="notes-only"] #speaker-controls {
278				padding-top: 40px;
279				top: 0;
280				left: 0;
281				width: 100%;
282				height: 100%;
283				font-size: 1.25em;
284			}
285
286			@media screen and (max-width: 1080px) {
287				body[data-speaker-layout="default"] #speaker-controls {
288					font-size: 16px;
289				}
290			}
291
292			@media screen and (max-width: 900px) {
293				body[data-speaker-layout="default"] #speaker-controls {
294					font-size: 14px;
295				}
296			}
297
298			@media screen and (max-width: 800px) {
299				body[data-speaker-layout="default"] #speaker-controls {
300					font-size: 12px;
301				}
302			}
303
304		</style>
305	</head>
306
307	<body>
308
309		<div id="connection-status">Loading speaker view...</div>
310
311		<div id="current-slide"></div>
312		<div id="upcoming-slide"><span class="overlay-element label">Upcoming</span></div>
313		<div id="speaker-controls">
314			<div class="speaker-controls-time">
315				<h4 class="label">Time <span class="reset-button">Click to Reset</span></h4>
316				<div class="clock">
317					<span class="clock-value">0:00 AM</span>
318				</div>
319				<div class="timer">
320					<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
321				</div>
322				<div class="clear"></div>
323
324				<h4 class="label pacing-title" style="display: none">Pacing – Time to finish current slide</h4>
325				<div class="pacing" style="display: none">
326					<span class="hours-value">00</span><span class="minutes-value">:00</span><span class="seconds-value">:00</span>
327				</div>
328			</div>
329
330			<div class="speaker-controls-notes hidden">
331				<h4 class="label">Notes</h4>
332				<div class="value"></div>
333			</div>
334		</div>
335		<div id="speaker-layout" class="overlay-element interactive">
336			<span class="speaker-layout-label"></span>
337			<select class="speaker-layout-dropdown"></select>
338		</div>
339
340		<script>
341
342			(function() {
343
344				var notes,
345					notesValue,
346					currentState,
347					currentSlide,
348					upcomingSlide,
349					layoutLabel,
350					layoutDropdown,
351					pendingCalls = {},
352					lastRevealApiCallId = 0,
353					connected = false
354
355				var connectionStatus = document.querySelector( '#connection-status' );
356
357				var SPEAKER_LAYOUTS = {
358					'default': 'Default',
359					'wide': 'Wide',
360					'tall': 'Tall',
361					'notes-only': 'Notes only'
362				};
363
364				setupLayout();
365
366				let openerOrigin;
367
368				try {
369					openerOrigin = window.opener.location.origin;
370				}
371				catch ( error ) { console.warn( error ) }
372
373				// In order to prevent XSS, the speaker view will only run if its
374				// opener has the same origin as itself
375				if( window.location.origin !== openerOrigin ) {
376					connectionStatus.innerHTML = 'Cross origin error.<br>The speaker window can only be opened from the same origin.';
377					return;
378				}
379
380				var connectionTimeout = setTimeout( function() {
381					connectionStatus.innerHTML = 'Error connecting to main window.<br>Please try closing and reopening the speaker view.';
382				}, 5000 );
383
384				window.addEventListener( 'message', function( event ) {
385
386					clearTimeout( connectionTimeout );
387					connectionStatus.style.display = 'none';
388
389					var data = JSON.parse( event.data );
390
391					// The overview mode is only useful to the reveal.js instance
392					// where navigation occurs so we don't sync it
393					if( data.state ) delete data.state.overview;
394
395					// Messages sent by the notes plugin inside of the main window
396					if( data && data.namespace === 'reveal-notes' ) {
397						if( data.type === 'connect' ) {
398							handleConnectMessage( data );
399						}
400						else if( data.type === 'state' ) {
401							handleStateMessage( data );
402						}
403						else if( data.type === 'return' ) {
404							pendingCalls[data.callId](data.result);
405							delete pendingCalls[data.callId];
406						}
407					}
408					// Messages sent by the reveal.js inside of the current slide preview
409					else if( data && data.namespace === 'reveal' ) {
410						if( /ready/.test( data.eventName ) ) {
411							// Send a message back to notify that the handshake is complete
412							window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
413						}
414						else if( /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
415
416							dispatchStateToMainWindow( data.state );
417
418						}
419					}
420
421				} );
422
423				/**
424				 * Updates the presentation in the main window to match the state
425				 * of the presentation in the notes window.
426				 */
427				const dispatchStateToMainWindow = debounce(( state ) => {
428					window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ state ]} ), '*' );
429				}, 500);
430
431				/**
432				 * Asynchronously calls the Reveal.js API of the main frame.
433				 */
434				function callRevealApi( methodName, methodArguments, callback ) {
435
436					var callId = ++lastRevealApiCallId;
437					pendingCalls[callId] = callback;
438					window.opener.postMessage( JSON.stringify( {
439						namespace: 'reveal-notes',
440						type: 'call',
441						callId: callId,
442						methodName: methodName,
443						arguments: methodArguments
444					} ), '*' );
445
446				}
447
448				/**
449				 * Called when the main window is trying to establish a
450				 * connection.
451				 */
452				function handleConnectMessage( data ) {
453
454					if( connected === false ) {
455						connected = true;
456
457						setupIframes( data );
458						setupKeyboard();
459						setupNotes();
460						setupTimer();
461						setupHeartbeat();
462					}
463
464				}
465
466				/**
467				 * Called when the main window sends an updated state.
468				 */
469				function handleStateMessage( data ) {
470
471					// Store the most recently set state to avoid circular loops
472					// applying the same state
473					currentState = JSON.stringify( data.state );
474
475					// No need for updating the notes in case of fragment changes
476					if ( data.notes ) {
477						notes.classList.remove( 'hidden' );
478						notesValue.style.whiteSpace = data.whitespace;
479						if( data.markdown ) {
480							notesValue.innerHTML = marked( data.notes );
481						}
482						else {
483							notesValue.innerHTML = data.notes;
484						}
485					}
486					else {
487						notes.classList.add( 'hidden' );
488					}
489
490					// Update the note slides
491					currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
492					upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
493					upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
494
495				}
496
497				// Limit to max one state update per X ms
498				handleStateMessage = debounce( handleStateMessage, 200 );
499
500				/**
501				 * Forward keyboard events to the current slide window.
502				 * This enables keyboard events to work even if focus
503				 * isn't set on the current slide iframe.
504				 *
505				 * Block F5 default handling, it reloads and disconnects
506				 * the speaker notes window.
507				 */
508				function setupKeyboard() {
509
510					document.addEventListener( 'keydown', function( event ) {
511						if( event.keyCode === 116 || ( event.metaKey && event.keyCode === 82 ) ) {
512							event.preventDefault();
513							return false;
514						}
515						currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
516					} );
517
518				}
519
520				/**
521				 * Creates the preview iframes.
522				 */
523				function setupIframes( data ) {
524
525					var params = [
526						'receiver',
527						'progress=false',
528						'history=false',
529						'transition=none',
530						'autoSlide=0',
531						'backgroundTransition=none'
532					].join( '&' );
533
534					var urlSeparator = /\?/.test(data.url) ? '&' : '?';
535					var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
536					var currentURL = data.url + urlSeparator + params + '&postMessageEvents=true' + hash;
537					var upcomingURL = data.url + urlSeparator + params + '&controls=false' + hash;
538
539					currentSlide = document.createElement( 'iframe' );
540					currentSlide.setAttribute( 'width', 1280 );
541					currentSlide.setAttribute( 'height', 1024 );
542					currentSlide.setAttribute( 'src', currentURL );
543					document.querySelector( '#current-slide' ).appendChild( currentSlide );
544
545					upcomingSlide = document.createElement( 'iframe' );
546					upcomingSlide.setAttribute( 'width', 640 );
547					upcomingSlide.setAttribute( 'height', 512 );
548					upcomingSlide.setAttribute( 'src', upcomingURL );
549					document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
550
551				}
552
553				/**
554				 * Setup the notes UI.
555				 */
556				function setupNotes() {
557
558					notes = document.querySelector( '.speaker-controls-notes' );
559					notesValue = document.querySelector( '.speaker-controls-notes .value' );
560
561				}
562
563				/**
564				 * We send out a heartbeat at all times to ensure we can
565				 * reconnect with the main presentation window after reloads.
566				 */
567				function setupHeartbeat() {
568
569					setInterval( () => {
570						window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
571					}, 1000 );
572
573				}
574
575				function getTimings( callback ) {
576
577					callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {
578						callRevealApi( 'getConfig', [], function ( config ) {
579							var totalTime = config.totalTime;
580							var minTimePerSlide = config.minimumTimePerSlide || 0;
581							var defaultTiming = config.defaultTiming;
582							if ((defaultTiming == null) && (totalTime == null)) {
583								callback(null);
584								return;
585							}
586							// Setting totalTime overrides defaultTiming
587							if (totalTime) {
588								defaultTiming = 0;
589							}
590							var timings = [];
591							for ( var i in slideAttributes ) {
592								var slide = slideAttributes[ i ];
593								var timing = defaultTiming;
594								if( slide.hasOwnProperty( 'data-timing' )) {
595									var t = slide[ 'data-timing' ];
596									timing = parseInt(t);
597									if( isNaN(timing) ) {
598										console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming);
599										timing = defaultTiming;
600									}
601								}
602								timings.push(timing);
603							}
604							if ( totalTime ) {
605								// After we've allocated time to individual slides, we summarize it and
606								// subtract it from the total time
607								var remainingTime = totalTime - timings.reduce( function(a, b) { return a + b; }, 0 );
608								// The remaining time is divided by the number of slides that have 0 seconds
609								// allocated at the moment, giving the average time-per-slide on the remaining slides
610								var remainingSlides = (timings.filter( function(x) { return x == 0 }) ).length
611								var timePerSlide = Math.round( remainingTime / remainingSlides, 0 )
612								// And now we replace every zero-value timing with that average
613								timings = timings.map( function(x) { return (x==0 ? timePerSlide : x) } );
614							}
615							var slidesUnderMinimum = timings.filter( function(x) { return (x < minTimePerSlide) } ).length
616							if ( slidesUnderMinimum ) {
617								message = "The pacing time for " + slidesUnderMinimum + " slide(s) is under the configured minimum of " + minTimePerSlide + " seconds. Check the data-timing attribute on individual slides, or consider increasing the totalTime or minimumTimePerSlide configuration options (or removing some slides).";
618								alert(message);
619							}
620							callback( timings );
621						} );
622					} );
623
624				}
625
626				/**
627				 * Return the number of seconds allocated for presenting
628				 * all slides up to and including this one.
629				 */
630				function getTimeAllocated( timings, callback ) {
631
632					callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
633						var allocated = 0;
634						for (var i in timings.slice(0, currentSlide + 1)) {
635							allocated += timings[i];
636						}
637						callback( allocated );
638					} );
639
640				}
641
642				/**
643				 * Create the timer and clock and start updating them
644				 * at an interval.
645				 */
646				function setupTimer() {
647
648					var start = new Date(),
649					timeEl = document.querySelector( '.speaker-controls-time' ),
650					clockEl = timeEl.querySelector( '.clock-value' ),
651					hoursEl = timeEl.querySelector( '.hours-value' ),
652					minutesEl = timeEl.querySelector( '.minutes-value' ),
653					secondsEl = timeEl.querySelector( '.seconds-value' ),
654					pacingTitleEl = timeEl.querySelector( '.pacing-title' ),
655					pacingEl = timeEl.querySelector( '.pacing' ),
656					pacingHoursEl = pacingEl.querySelector( '.hours-value' ),
657					pacingMinutesEl = pacingEl.querySelector( '.minutes-value' ),
658					pacingSecondsEl = pacingEl.querySelector( '.seconds-value' );
659
660					var timings = null;
661					getTimings( function ( _timings ) {
662
663						timings = _timings;
664						if (_timings !== null) {
665							pacingTitleEl.style.removeProperty('display');
666							pacingEl.style.removeProperty('display');
667						}
668
669						// Update once directly
670						_updateTimer();
671
672						// Then update every second
673						setInterval( _updateTimer, 1000 );
674
675					} );
676
677
678					function _resetTimer() {
679
680						if (timings == null) {
681							start = new Date();
682							_updateTimer();
683						}
684						else {
685							// Reset timer to beginning of current slide
686							getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
687								var slideEndTiming = slideEndTimingSeconds * 1000;
688								callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
689									var currentSlideTiming = timings[currentSlide] * 1000;
690									var previousSlidesTiming = slideEndTiming - currentSlideTiming;
691									var now = new Date();
692									start = new Date(now.getTime() - previousSlidesTiming);
693									_updateTimer();
694								} );
695							} );
696						}
697
698					}
699
700					timeEl.addEventListener( 'click', function() {
701						_resetTimer();
702						return false;
703					} );
704
705					function _displayTime( hrEl, minEl, secEl, time) {
706
707						var sign = Math.sign(time) == -1 ? "-" : "";
708						time = Math.abs(Math.round(time / 1000));
709						var seconds = time % 60;
710						var minutes = Math.floor( time / 60 ) % 60 ;
711						var hours = Math.floor( time / ( 60 * 60 )) ;
712						hrEl.innerHTML = sign + zeroPadInteger( hours );
713						if (hours == 0) {
714							hrEl.classList.add( 'mute' );
715						}
716						else {
717							hrEl.classList.remove( 'mute' );
718						}
719						minEl.innerHTML = ':' + zeroPadInteger( minutes );
720						if (hours == 0 && minutes == 0) {
721							minEl.classList.add( 'mute' );
722						}
723						else {
724							minEl.classList.remove( 'mute' );
725						}
726						secEl.innerHTML = ':' + zeroPadInteger( seconds );
727					}
728
729					function _updateTimer() {
730
731						var diff, hours, minutes, seconds,
732						now = new Date();
733
734						diff = now.getTime() - start.getTime();
735
736						clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
737						_displayTime( hoursEl, minutesEl, secondsEl, diff );
738						if (timings !== null) {
739							_updatePacing(diff);
740						}
741
742					}
743
744					function _updatePacing(diff) {
745
746						getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
747							var slideEndTiming = slideEndTimingSeconds * 1000;
748
749							callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
750								var currentSlideTiming = timings[currentSlide] * 1000;
751								var timeLeftCurrentSlide = slideEndTiming - diff;
752								if (timeLeftCurrentSlide < 0) {
753									pacingEl.className = 'pacing behind';
754								}
755								else if (timeLeftCurrentSlide < currentSlideTiming) {
756									pacingEl.className = 'pacing on-track';
757								}
758								else {
759									pacingEl.className = 'pacing ahead';
760								}
761								_displayTime( pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide );
762							} );
763						} );
764					}
765
766				}
767
768				/**
769				 * Sets up the speaker view layout and layout selector.
770				 */
771				function setupLayout() {
772
773					layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
774					layoutLabel = document.querySelector( '.speaker-layout-label' );
775
776					// Render the list of available layouts
777					for( var id in SPEAKER_LAYOUTS ) {
778						var option = document.createElement( 'option' );
779						option.setAttribute( 'value', id );
780						option.textContent = SPEAKER_LAYOUTS[ id ];
781						layoutDropdown.appendChild( option );
782					}
783
784					// Monitor the dropdown for changes
785					layoutDropdown.addEventListener( 'change', function( event ) {
786
787						setLayout( layoutDropdown.value );
788
789					}, false );
790
791					// Restore any currently persisted layout
792					setLayout( getLayout() );
793
794				}
795
796				/**
797				 * Sets a new speaker view layout. The layout is persisted
798				 * in local storage.
799				 */
800				function setLayout( value ) {
801
802					var title = SPEAKER_LAYOUTS[ value ];
803
804					layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
805					layoutDropdown.value = value;
806
807					document.body.setAttribute( 'data-speaker-layout', value );
808
809					// Persist locally
810					if( supportsLocalStorage() ) {
811						window.localStorage.setItem( 'reveal-speaker-layout', value );
812					}
813
814				}
815
816				/**
817				 * Returns the ID of the most recently set speaker layout
818				 * or our default layout if none has been set.
819				 */
820				function getLayout() {
821
822					if( supportsLocalStorage() ) {
823						var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
824						if( layout ) {
825							return layout;
826						}
827					}
828
829					// Default to the first record in the layouts hash
830					for( var id in SPEAKER_LAYOUTS ) {
831						return id;
832					}
833
834				}
835
836				function supportsLocalStorage() {
837
838					try {
839						localStorage.setItem('test', 'test');
840						localStorage.removeItem('test');
841						return true;
842					}
843					catch( e ) {
844						return false;
845					}
846
847				}
848
849				function zeroPadInteger( num ) {
850
851					var str = '00' + parseInt( num );
852					return str.substring( str.length - 2 );
853
854				}
855
856				/**
857				 * Limits the frequency at which a function can be called.
858				 */
859				function debounce( fn, ms ) {
860
861					var lastTime = 0,
862						timeout;
863
864					return function() {
865
866						var args = arguments;
867						var context = this;
868
869						clearTimeout( timeout );
870
871						var timeSinceLastCall = Date.now() - lastTime;
872						if( timeSinceLastCall > ms ) {
873							fn.apply( context, args );
874							lastTime = Date.now();
875						}
876						else {
877							timeout = setTimeout( function() {
878								fn.apply( context, args );
879								lastTime = Date.now();
880							}, ms - timeSinceLastCall );
881						}
882
883					}
884
885				}
886
887			})();
888
889		</script>
890	</body>
891</html>