presentations

Presentations
Log | Files | Refs

notes.html (20978B)


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