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>