crossnote 0.8.8 → 0.8.10

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