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