github.com/mweagle/Sparta@v1.15.0/docs_source/static/presentations/reveal.js-3.9.2/plugin/notes/notes.html (about)

     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 totalTime = config.totalTime;
   543  							var minTimePerSlide = config.minimumTimePerSlide || 0;
   544  							var defaultTiming = config.defaultTiming;
   545  							if ((defaultTiming == null) && (totalTime == null)) {
   546  								callback(null);
   547  								return;
   548  							}
   549  							// Setting totalTime overrides defaultTiming
   550  							if (totalTime) {
   551  								defaultTiming = 0;
   552  							}
   553  							var timings = [];
   554  							for ( var i in slideAttributes ) {
   555  								var slide = slideAttributes[ i ];
   556  								var timing = defaultTiming;
   557  								if( slide.hasOwnProperty( 'data-timing' )) {
   558  									var t = slide[ 'data-timing' ];
   559  									timing = parseInt(t);
   560  									if( isNaN(timing) ) {
   561  										console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming);
   562  										timing = defaultTiming;
   563  									}
   564  								}
   565  								timings.push(timing);
   566  							}
   567  							if ( totalTime ) {
   568  								// After we've allocated time to individual slides, we summarize it and
   569  								// subtract it from the total time
   570  								var remainingTime = totalTime - timings.reduce( function(a, b) { return a + b; }, 0 );
   571  								// The remaining time is divided by the number of slides that have 0 seconds
   572  								// allocated at the moment, giving the average time-per-slide on the remaining slides
   573  								var remainingSlides = (timings.filter( function(x) { return x == 0 }) ).length
   574  								var timePerSlide = Math.round( remainingTime / remainingSlides, 0 )
   575  								// And now we replace every zero-value timing with that average
   576  								timings = timings.map( function(x) { return (x==0 ? timePerSlide : x) } );
   577  							}
   578  							var slidesUnderMinimum = timings.filter( function(x) { return (x < minTimePerSlide) } ).length
   579  							if ( slidesUnderMinimum ) {
   580  								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).";
   581  								alert(message);
   582  							}
   583  							callback( timings );
   584  						} );
   585  					} );
   586  
   587  				}
   588  
   589  				/**
   590  				 * Return the number of seconds allocated for presenting
   591  				 * all slides up to and including this one.
   592  				 */
   593  				function getTimeAllocated( timings, callback ) {
   594  
   595  					callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
   596  						var allocated = 0;
   597  						for (var i in timings.slice(0, currentSlide + 1)) {
   598  							allocated += timings[i];
   599  						}
   600  						callback( allocated );
   601  					} );
   602  
   603  				}
   604  
   605  				/**
   606  				 * Create the timer and clock and start updating them
   607  				 * at an interval.
   608  				 */
   609  				function setupTimer() {
   610  
   611  					var start = new Date(),
   612  					timeEl = document.querySelector( '.speaker-controls-time' ),
   613  					clockEl = timeEl.querySelector( '.clock-value' ),
   614  					hoursEl = timeEl.querySelector( '.hours-value' ),
   615  					minutesEl = timeEl.querySelector( '.minutes-value' ),
   616  					secondsEl = timeEl.querySelector( '.seconds-value' ),
   617  					pacingTitleEl = timeEl.querySelector( '.pacing-title' ),
   618  					pacingEl = timeEl.querySelector( '.pacing' ),
   619  					pacingHoursEl = pacingEl.querySelector( '.hours-value' ),
   620  					pacingMinutesEl = pacingEl.querySelector( '.minutes-value' ),
   621  					pacingSecondsEl = pacingEl.querySelector( '.seconds-value' );
   622  
   623  					var timings = null;
   624  					getTimings( function ( _timings ) {
   625  
   626  						timings = _timings;
   627  						if (_timings !== null) {
   628  							pacingTitleEl.style.removeProperty('display');
   629  							pacingEl.style.removeProperty('display');
   630  						}
   631  
   632  						// Update once directly
   633  						_updateTimer();
   634  
   635  						// Then update every second
   636  						setInterval( _updateTimer, 1000 );
   637  
   638  					} );
   639  
   640  
   641  					function _resetTimer() {
   642  
   643  						if (timings == null) {
   644  							start = new Date();
   645  							_updateTimer();
   646  						}
   647  						else {
   648  							// Reset timer to beginning of current slide
   649  							getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
   650  								var slideEndTiming = slideEndTimingSeconds * 1000;
   651  								callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
   652  									var currentSlideTiming = timings[currentSlide] * 1000;
   653  									var previousSlidesTiming = slideEndTiming - currentSlideTiming;
   654  									var now = new Date();
   655  									start = new Date(now.getTime() - previousSlidesTiming);
   656  									_updateTimer();
   657  								} );
   658  							} );
   659  						}
   660  
   661  					}
   662  
   663  					timeEl.addEventListener( 'click', function() {
   664  						_resetTimer();
   665  						return false;
   666  					} );
   667  
   668  					function _displayTime( hrEl, minEl, secEl, time) {
   669  
   670  						var sign = Math.sign(time) == -1 ? "-" : "";
   671  						time = Math.abs(Math.round(time / 1000));
   672  						var seconds = time % 60;
   673  						var minutes = Math.floor( time / 60 ) % 60 ;
   674  						var hours = Math.floor( time / ( 60 * 60 )) ;
   675  						hrEl.innerHTML = sign + zeroPadInteger( hours );
   676  						if (hours == 0) {
   677  							hrEl.classList.add( 'mute' );
   678  						}
   679  						else {
   680  							hrEl.classList.remove( 'mute' );
   681  						}
   682  						minEl.innerHTML = ':' + zeroPadInteger( minutes );
   683  						if (hours == 0 && minutes == 0) {
   684  							minEl.classList.add( 'mute' );
   685  						}
   686  						else {
   687  							minEl.classList.remove( 'mute' );
   688  						}
   689  						secEl.innerHTML = ':' + zeroPadInteger( seconds );
   690  					}
   691  
   692  					function _updateTimer() {
   693  
   694  						var diff, hours, minutes, seconds,
   695  						now = new Date();
   696  
   697  						diff = now.getTime() - start.getTime();
   698  
   699  						clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
   700  						_displayTime( hoursEl, minutesEl, secondsEl, diff );
   701  						if (timings !== null) {
   702  							_updatePacing(diff);
   703  						}
   704  
   705  					}
   706  
   707  					function _updatePacing(diff) {
   708  
   709  						getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
   710  							var slideEndTiming = slideEndTimingSeconds * 1000;
   711  
   712  							callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
   713  								var currentSlideTiming = timings[currentSlide] * 1000;
   714  								var timeLeftCurrentSlide = slideEndTiming - diff;
   715  								if (timeLeftCurrentSlide < 0) {
   716  									pacingEl.className = 'pacing behind';
   717  								}
   718  								else if (timeLeftCurrentSlide < currentSlideTiming) {
   719  									pacingEl.className = 'pacing on-track';
   720  								}
   721  								else {
   722  									pacingEl.className = 'pacing ahead';
   723  								}
   724  								_displayTime( pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide );
   725  							} );
   726  						} );
   727  					}
   728  
   729  				}
   730  
   731  				/**
   732  				 * Sets up the speaker view layout and layout selector.
   733  				 */
   734  				function setupLayout() {
   735  
   736  					layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
   737  					layoutLabel = document.querySelector( '.speaker-layout-label' );
   738  
   739  					// Render the list of available layouts
   740  					for( var id in SPEAKER_LAYOUTS ) {
   741  						var option = document.createElement( 'option' );
   742  						option.setAttribute( 'value', id );
   743  						option.textContent = SPEAKER_LAYOUTS[ id ];
   744  						layoutDropdown.appendChild( option );
   745  					}
   746  
   747  					// Monitor the dropdown for changes
   748  					layoutDropdown.addEventListener( 'change', function( event ) {
   749  
   750  						setLayout( layoutDropdown.value );
   751  
   752  					}, false );
   753  
   754  					// Restore any currently persisted layout
   755  					setLayout( getLayout() );
   756  
   757  				}
   758  
   759  				/**
   760  				 * Sets a new speaker view layout. The layout is persisted
   761  				 * in local storage.
   762  				 */
   763  				function setLayout( value ) {
   764  
   765  					var title = SPEAKER_LAYOUTS[ value ];
   766  
   767  					layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
   768  					layoutDropdown.value = value;
   769  
   770  					document.body.setAttribute( 'data-speaker-layout', value );
   771  
   772  					// Persist locally
   773  					if( supportsLocalStorage() ) {
   774  						window.localStorage.setItem( 'reveal-speaker-layout', value );
   775  					}
   776  
   777  				}
   778  
   779  				/**
   780  				 * Returns the ID of the most recently set speaker layout
   781  				 * or our default layout if none has been set.
   782  				 */
   783  				function getLayout() {
   784  
   785  					if( supportsLocalStorage() ) {
   786  						var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
   787  						if( layout ) {
   788  							return layout;
   789  						}
   790  					}
   791  
   792  					// Default to the first record in the layouts hash
   793  					for( var id in SPEAKER_LAYOUTS ) {
   794  						return id;
   795  					}
   796  
   797  				}
   798  
   799  				function supportsLocalStorage() {
   800  
   801  					try {
   802  						localStorage.setItem('test', 'test');
   803  						localStorage.removeItem('test');
   804  						return true;
   805  					}
   806  					catch( e ) {
   807  						return false;
   808  					}
   809  
   810  				}
   811  
   812  				function zeroPadInteger( num ) {
   813  
   814  					var str = '00' + parseInt( num );
   815  					return str.substring( str.length - 2 );
   816  
   817  				}
   818  
   819  				/**
   820  				 * Limits the frequency at which a function can be called.
   821  				 */
   822  				function debounce( fn, ms ) {
   823  
   824  					var lastTime = 0,
   825  						timeout;
   826  
   827  					return function() {
   828  
   829  						var args = arguments;
   830  						var context = this;
   831  
   832  						clearTimeout( timeout );
   833  
   834  						var timeSinceLastCall = Date.now() - lastTime;
   835  						if( timeSinceLastCall > ms ) {
   836  							fn.apply( context, args );
   837  							lastTime = Date.now();
   838  						}
   839  						else {
   840  							timeout = setTimeout( function() {
   841  								fn.apply( context, args );
   842  								lastTime = Date.now();
   843  							}, ms - timeSinceLastCall );
   844  						}
   845  
   846  					}
   847  
   848  				}
   849  
   850  			})();
   851  
   852  		</script>
   853  	</body>
   854  </html>