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>