lazyslides 0.1.0

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -0
  3. package/_includes/slides/_nested-list.njk +26 -0
  4. package/_includes/slides/agenda.njk +17 -0
  5. package/_includes/slides/center.njk +16 -0
  6. package/_includes/slides/code.njk +15 -0
  7. package/_includes/slides/columns.njk +35 -0
  8. package/_includes/slides/comparison.njk +29 -0
  9. package/_includes/slides/content.njk +22 -0
  10. package/_includes/slides/footer.njk +58 -0
  11. package/_includes/slides/funnel.njk +30 -0
  12. package/_includes/slides/hero.njk +27 -0
  13. package/_includes/slides/image-overlay.njk +21 -0
  14. package/_includes/slides/metrics.njk +27 -0
  15. package/_includes/slides/quote.njk +17 -0
  16. package/_includes/slides/section.njk +6 -0
  17. package/_includes/slides/split-wide.njk +30 -0
  18. package/_includes/slides/split.njk +30 -0
  19. package/_includes/slides/table.njk +31 -0
  20. package/_includes/slides/timeline.njk +30 -0
  21. package/_includes/slides/title.njk +17 -0
  22. package/_layouts/default.njk +20 -0
  23. package/_layouts/presentation.njk +240 -0
  24. package/assets/css/themes/default.css +62 -0
  25. package/assets/css/vendor/glightbox.min.css +1 -0
  26. package/assets/js/vendor/glightbox.min.js +1 -0
  27. package/assets/reveal.js/LICENSE +19 -0
  28. package/assets/reveal.js/dist/reset.css +30 -0
  29. package/assets/reveal.js/dist/reveal.css +8 -0
  30. package/assets/reveal.js/dist/reveal.esm.js +9 -0
  31. package/assets/reveal.js/dist/reveal.esm.js.map +1 -0
  32. package/assets/reveal.js/dist/reveal.js +9 -0
  33. package/assets/reveal.js/dist/reveal.js.map +1 -0
  34. package/assets/reveal.js/dist/theme/beige.css +366 -0
  35. package/assets/reveal.js/dist/theme/black-contrast.css +362 -0
  36. package/assets/reveal.js/dist/theme/black.css +359 -0
  37. package/assets/reveal.js/dist/theme/blood.css +392 -0
  38. package/assets/reveal.js/dist/theme/dracula.css +385 -0
  39. package/assets/reveal.js/dist/theme/fonts/league-gothic/LICENSE +2 -0
  40. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.css +10 -0
  41. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.eot +0 -0
  42. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.ttf +0 -0
  43. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.woff +0 -0
  44. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/LICENSE +45 -0
  45. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.eot +0 -0
  46. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.ttf +0 -0
  47. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.woff +0 -0
  48. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.eot +0 -0
  49. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.ttf +0 -0
  50. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.woff +0 -0
  51. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.eot +0 -0
  52. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.ttf +0 -0
  53. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.woff +0 -0
  54. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.eot +0 -0
  55. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.ttf +0 -0
  56. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff +0 -0
  57. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro.css +39 -0
  58. package/assets/reveal.js/dist/theme/league.css +368 -0
  59. package/assets/reveal.js/dist/theme/moon.css +362 -0
  60. package/assets/reveal.js/dist/theme/night.css +360 -0
  61. package/assets/reveal.js/dist/theme/serif.css +363 -0
  62. package/assets/reveal.js/dist/theme/simple.css +362 -0
  63. package/assets/reveal.js/dist/theme/sky.css +370 -0
  64. package/assets/reveal.js/dist/theme/solarized.css +363 -0
  65. package/assets/reveal.js/dist/theme/white-contrast.css +362 -0
  66. package/assets/reveal.js/dist/theme/white.css +359 -0
  67. package/assets/reveal.js/dist/theme/white_contrast_compact_verbatim_headers.css +360 -0
  68. package/assets/reveal.js/plugin/highlight/highlight.esm.js +5 -0
  69. package/assets/reveal.js/plugin/highlight/highlight.js +5 -0
  70. package/assets/reveal.js/plugin/highlight/monokai.css +71 -0
  71. package/assets/reveal.js/plugin/highlight/plugin.js +439 -0
  72. package/assets/reveal.js/plugin/highlight/zenburn.css +80 -0
  73. package/assets/reveal.js/plugin/markdown/markdown.esm.js +7 -0
  74. package/assets/reveal.js/plugin/markdown/markdown.js +7 -0
  75. package/assets/reveal.js/plugin/markdown/plugin.js +491 -0
  76. package/assets/reveal.js/plugin/notes/notes.esm.js +1 -0
  77. package/assets/reveal.js/plugin/notes/notes.js +1 -0
  78. package/assets/reveal.js/plugin/notes/plugin.js +267 -0
  79. package/assets/reveal.js/plugin/notes/speaker-view.html +898 -0
  80. package/cli.js +80 -0
  81. package/data/site.json +3 -0
  82. package/index.js +153 -0
  83. package/lib/export-pdf.js +137 -0
  84. package/lib/init.js +154 -0
  85. package/lib/renumber.js +181 -0
  86. package/lib/validate.js +196 -0
  87. package/package.json +76 -0
  88. package/scaffold/CLAUDE.md +283 -0
  89. package/scaffold/README.md +29 -0
  90. package/scaffold/_template/index.md +78 -0
  91. package/scaffold/_template/outline.md +32 -0
  92. package/scaffold/claude-commands/add-slide.md +61 -0
  93. package/scaffold/claude-commands/create-outline.md +75 -0
  94. package/scaffold/claude-commands/new-presentation.md +218 -0
  95. package/scaffold/claude-commands/refine-slides.md +62 -0
  96. package/scaffold/claude-commands/research-topic.md +66 -0
  97. package/scaffold/claude-commands/validate.md +47 -0
  98. package/scaffold/claude-settings.json +7 -0
  99. package/scaffold/eleventy.config.js +11 -0
  100. package/scaffold/gitignore +5 -0
  101. package/scaffold/my-first-deck/index.md +26 -0
  102. package/scaffold/nvmrc +1 -0
  103. package/scaffold/package.json.tmpl +25 -0
  104. package/scaffold/presentations.11tydata.js +11 -0
  105. package/scaffold/styles.css +3 -0
  106. package/src/styles.css +2077 -0
@@ -0,0 +1,898 @@
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
+ // Validate the origin of all messages to avoid parsing messages
387
+ // that aren't meant for us. Ignore when running off file:// so
388
+ // that the speaker view continues to work without a web server.
389
+ if( window.location.origin !== event.origin && window.location.origin !== 'file://' ) {
390
+ return
391
+ }
392
+
393
+ clearTimeout( connectionTimeout );
394
+ connectionStatus.style.display = 'none';
395
+
396
+ var data = JSON.parse( event.data );
397
+
398
+ // The overview mode is only useful to the reveal.js instance
399
+ // where navigation occurs so we don't sync it
400
+ if( data.state ) delete data.state.overview;
401
+
402
+ // Messages sent by the notes plugin inside of the main window
403
+ if( data && data.namespace === 'reveal-notes' ) {
404
+ if( data.type === 'connect' ) {
405
+ handleConnectMessage( data );
406
+ }
407
+ else if( data.type === 'state' ) {
408
+ handleStateMessage( data );
409
+ }
410
+ else if( data.type === 'return' ) {
411
+ pendingCalls[data.callId](data.result);
412
+ delete pendingCalls[data.callId];
413
+ }
414
+ }
415
+ // Messages sent by the reveal.js inside of the current slide preview
416
+ else if( data && data.namespace === 'reveal' ) {
417
+ if( /ready/.test( data.eventName ) ) {
418
+ // Send a message back to notify that the handshake is complete
419
+ window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
420
+ }
421
+ else if( /slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
422
+
423
+ dispatchStateToMainWindow( data.state );
424
+
425
+ }
426
+ }
427
+
428
+ } );
429
+
430
+ /**
431
+ * Updates the presentation in the main window to match the state
432
+ * of the presentation in the notes window.
433
+ */
434
+ const dispatchStateToMainWindow = debounce(( state ) => {
435
+ window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ state ]} ), '*' );
436
+ }, 500);
437
+
438
+ /**
439
+ * Asynchronously calls the Reveal.js API of the main frame.
440
+ */
441
+ function callRevealApi( methodName, methodArguments, callback ) {
442
+
443
+ var callId = ++lastRevealApiCallId;
444
+ pendingCalls[callId] = callback;
445
+ window.opener.postMessage( JSON.stringify( {
446
+ namespace: 'reveal-notes',
447
+ type: 'call',
448
+ callId: callId,
449
+ methodName: methodName,
450
+ arguments: methodArguments
451
+ } ), '*' );
452
+
453
+ }
454
+
455
+ /**
456
+ * Called when the main window is trying to establish a
457
+ * connection.
458
+ */
459
+ function handleConnectMessage( data ) {
460
+
461
+ if( connected === false ) {
462
+ connected = true;
463
+
464
+ setupIframes( data );
465
+ setupKeyboard();
466
+ setupNotes();
467
+ setupTimer();
468
+ setupHeartbeat();
469
+ }
470
+
471
+ }
472
+
473
+ /**
474
+ * Called when the main window sends an updated state.
475
+ */
476
+ function handleStateMessage( data ) {
477
+
478
+ // Store the most recently set state to avoid circular loops
479
+ // applying the same state
480
+ currentState = JSON.stringify( data.state );
481
+
482
+ // No need for updating the notes in case of fragment changes
483
+ if ( data.notes ) {
484
+ notes.classList.remove( 'hidden' );
485
+ notesValue.style.whiteSpace = data.whitespace;
486
+ if( data.markdown ) {
487
+ notesValue.innerHTML = marked( data.notes );
488
+ }
489
+ else {
490
+ notesValue.innerHTML = data.notes;
491
+ }
492
+ }
493
+ else {
494
+ notes.classList.add( 'hidden' );
495
+ }
496
+
497
+ // Update the note slides
498
+ currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
499
+ upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
500
+ upcomingSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
501
+
502
+ }
503
+
504
+ // Limit to max one state update per X ms
505
+ handleStateMessage = debounce( handleStateMessage, 200 );
506
+
507
+ /**
508
+ * Forward keyboard events to the current slide window.
509
+ * This enables keyboard events to work even if focus
510
+ * isn't set on the current slide iframe.
511
+ *
512
+ * Block F5 default handling, it reloads and disconnects
513
+ * the speaker notes window.
514
+ */
515
+ function setupKeyboard() {
516
+
517
+ document.addEventListener( 'keydown', function( event ) {
518
+ if( event.keyCode === 116 || ( event.metaKey && event.keyCode === 82 ) ) {
519
+ event.preventDefault();
520
+ return false;
521
+ }
522
+ currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'triggerKey', args: [ event.keyCode ] }), '*' );
523
+ } );
524
+
525
+ }
526
+
527
+ /**
528
+ * Creates the preview iframes.
529
+ */
530
+ function setupIframes( data ) {
531
+
532
+ var params = [
533
+ 'receiver',
534
+ 'progress=false',
535
+ 'history=false',
536
+ 'transition=none',
537
+ 'autoSlide=0',
538
+ 'backgroundTransition=none'
539
+ ].join( '&' );
540
+
541
+ var urlSeparator = /\?/.test(data.url) ? '&' : '?';
542
+ var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
543
+ var currentURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&postMessageEvents=true' + hash;
544
+ var upcomingURL = data.url + urlSeparator + params + '&scrollActivationWidth=false&controls=false' + hash;
545
+
546
+ currentSlide = document.createElement( 'iframe' );
547
+ currentSlide.setAttribute( 'width', 1280 );
548
+ currentSlide.setAttribute( 'height', 1024 );
549
+ currentSlide.setAttribute( 'src', currentURL );
550
+ document.querySelector( '#current-slide' ).appendChild( currentSlide );
551
+
552
+ upcomingSlide = document.createElement( 'iframe' );
553
+ upcomingSlide.setAttribute( 'width', 640 );
554
+ upcomingSlide.setAttribute( 'height', 512 );
555
+ upcomingSlide.setAttribute( 'src', upcomingURL );
556
+ document.querySelector( '#upcoming-slide' ).appendChild( upcomingSlide );
557
+
558
+ }
559
+
560
+ /**
561
+ * Setup the notes UI.
562
+ */
563
+ function setupNotes() {
564
+
565
+ notes = document.querySelector( '.speaker-controls-notes' );
566
+ notesValue = document.querySelector( '.speaker-controls-notes .value' );
567
+
568
+ }
569
+
570
+ /**
571
+ * We send out a heartbeat at all times to ensure we can
572
+ * reconnect with the main presentation window after reloads.
573
+ */
574
+ function setupHeartbeat() {
575
+
576
+ setInterval( () => {
577
+ window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
578
+ }, 1000 );
579
+
580
+ }
581
+
582
+ function getTimings( callback ) {
583
+
584
+ callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {
585
+ callRevealApi( 'getConfig', [], function ( config ) {
586
+ var totalTime = config.totalTime;
587
+ var minTimePerSlide = config.minimumTimePerSlide || 0;
588
+ var defaultTiming = config.defaultTiming;
589
+ if ((defaultTiming == null) && (totalTime == null)) {
590
+ callback(null);
591
+ return;
592
+ }
593
+ // Setting totalTime overrides defaultTiming
594
+ if (totalTime) {
595
+ defaultTiming = 0;
596
+ }
597
+ var timings = [];
598
+ for ( var i in slideAttributes ) {
599
+ var slide = slideAttributes[ i ];
600
+ var timing = defaultTiming;
601
+ if( slide.hasOwnProperty( 'data-timing' )) {
602
+ var t = slide[ 'data-timing' ];
603
+ timing = parseInt(t);
604
+ if( isNaN(timing) ) {
605
+ console.warn("Could not parse timing '" + t + "' of slide " + i + "; using default of " + defaultTiming);
606
+ timing = defaultTiming;
607
+ }
608
+ }
609
+ timings.push(timing);
610
+ }
611
+ if ( totalTime ) {
612
+ // After we've allocated time to individual slides, we summarize it and
613
+ // subtract it from the total time
614
+ var remainingTime = totalTime - timings.reduce( function(a, b) { return a + b; }, 0 );
615
+ // The remaining time is divided by the number of slides that have 0 seconds
616
+ // allocated at the moment, giving the average time-per-slide on the remaining slides
617
+ var remainingSlides = (timings.filter( function(x) { return x == 0 }) ).length
618
+ var timePerSlide = Math.round( remainingTime / remainingSlides, 0 )
619
+ // And now we replace every zero-value timing with that average
620
+ timings = timings.map( function(x) { return (x==0 ? timePerSlide : x) } );
621
+ }
622
+ var slidesUnderMinimum = timings.filter( function(x) { return (x < minTimePerSlide) } ).length
623
+ if ( slidesUnderMinimum ) {
624
+ 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).";
625
+ alert(message);
626
+ }
627
+ callback( timings );
628
+ } );
629
+ } );
630
+
631
+ }
632
+
633
+ /**
634
+ * Return the number of seconds allocated for presenting
635
+ * all slides up to and including this one.
636
+ */
637
+ function getTimeAllocated( timings, callback ) {
638
+
639
+ callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
640
+ var allocated = 0;
641
+ for (var i in timings.slice(0, currentSlide + 1)) {
642
+ allocated += timings[i];
643
+ }
644
+ callback( allocated );
645
+ } );
646
+
647
+ }
648
+
649
+ /**
650
+ * Create the timer and clock and start updating them
651
+ * at an interval.
652
+ */
653
+ function setupTimer() {
654
+
655
+ var start = new Date(),
656
+ timeEl = document.querySelector( '.speaker-controls-time' ),
657
+ clockEl = timeEl.querySelector( '.clock-value' ),
658
+ hoursEl = timeEl.querySelector( '.hours-value' ),
659
+ minutesEl = timeEl.querySelector( '.minutes-value' ),
660
+ secondsEl = timeEl.querySelector( '.seconds-value' ),
661
+ pacingTitleEl = timeEl.querySelector( '.pacing-title' ),
662
+ pacingEl = timeEl.querySelector( '.pacing' ),
663
+ pacingHoursEl = pacingEl.querySelector( '.hours-value' ),
664
+ pacingMinutesEl = pacingEl.querySelector( '.minutes-value' ),
665
+ pacingSecondsEl = pacingEl.querySelector( '.seconds-value' );
666
+
667
+ var timings = null;
668
+ getTimings( function ( _timings ) {
669
+
670
+ timings = _timings;
671
+ if (_timings !== null) {
672
+ pacingTitleEl.style.removeProperty('display');
673
+ pacingEl.style.removeProperty('display');
674
+ }
675
+
676
+ // Update once directly
677
+ _updateTimer();
678
+
679
+ // Then update every second
680
+ setInterval( _updateTimer, 1000 );
681
+
682
+ } );
683
+
684
+
685
+ function _resetTimer() {
686
+
687
+ if (timings == null) {
688
+ start = new Date();
689
+ _updateTimer();
690
+ }
691
+ else {
692
+ // Reset timer to beginning of current slide
693
+ getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
694
+ var slideEndTiming = slideEndTimingSeconds * 1000;
695
+ callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
696
+ var currentSlideTiming = timings[currentSlide] * 1000;
697
+ var previousSlidesTiming = slideEndTiming - currentSlideTiming;
698
+ var now = new Date();
699
+ start = new Date(now.getTime() - previousSlidesTiming);
700
+ _updateTimer();
701
+ } );
702
+ } );
703
+ }
704
+
705
+ }
706
+
707
+ timeEl.addEventListener( 'click', function() {
708
+ _resetTimer();
709
+ return false;
710
+ } );
711
+
712
+ function _displayTime( hrEl, minEl, secEl, time) {
713
+
714
+ var sign = Math.sign(time) == -1 ? "-" : "";
715
+ time = Math.abs(Math.round(time / 1000));
716
+ var seconds = time % 60;
717
+ var minutes = Math.floor( time / 60 ) % 60 ;
718
+ var hours = Math.floor( time / ( 60 * 60 )) ;
719
+ hrEl.innerHTML = sign + zeroPadInteger( hours );
720
+ if (hours == 0) {
721
+ hrEl.classList.add( 'mute' );
722
+ }
723
+ else {
724
+ hrEl.classList.remove( 'mute' );
725
+ }
726
+ minEl.innerHTML = ':' + zeroPadInteger( minutes );
727
+ if (hours == 0 && minutes == 0) {
728
+ minEl.classList.add( 'mute' );
729
+ }
730
+ else {
731
+ minEl.classList.remove( 'mute' );
732
+ }
733
+ secEl.innerHTML = ':' + zeroPadInteger( seconds );
734
+ }
735
+
736
+ function _updateTimer() {
737
+
738
+ var diff, hours, minutes, seconds,
739
+ now = new Date();
740
+
741
+ diff = now.getTime() - start.getTime();
742
+
743
+ clockEl.innerHTML = now.toLocaleTimeString( 'en-US', { hour12: true, hour: '2-digit', minute:'2-digit' } );
744
+ _displayTime( hoursEl, minutesEl, secondsEl, diff );
745
+ if (timings !== null) {
746
+ _updatePacing(diff);
747
+ }
748
+
749
+ }
750
+
751
+ function _updatePacing(diff) {
752
+
753
+ getTimeAllocated( timings, function ( slideEndTimingSeconds ) {
754
+ var slideEndTiming = slideEndTimingSeconds * 1000;
755
+
756
+ callRevealApi( 'getSlidePastCount', [], function ( currentSlide ) {
757
+ var currentSlideTiming = timings[currentSlide] * 1000;
758
+ var timeLeftCurrentSlide = slideEndTiming - diff;
759
+ if (timeLeftCurrentSlide < 0) {
760
+ pacingEl.className = 'pacing behind';
761
+ }
762
+ else if (timeLeftCurrentSlide < currentSlideTiming) {
763
+ pacingEl.className = 'pacing on-track';
764
+ }
765
+ else {
766
+ pacingEl.className = 'pacing ahead';
767
+ }
768
+ _displayTime( pacingHoursEl, pacingMinutesEl, pacingSecondsEl, timeLeftCurrentSlide );
769
+ } );
770
+ } );
771
+ }
772
+
773
+ }
774
+
775
+ /**
776
+ * Sets up the speaker view layout and layout selector.
777
+ */
778
+ function setupLayout() {
779
+
780
+ layoutDropdown = document.querySelector( '.speaker-layout-dropdown' );
781
+ layoutLabel = document.querySelector( '.speaker-layout-label' );
782
+
783
+ // Render the list of available layouts
784
+ for( var id in SPEAKER_LAYOUTS ) {
785
+ var option = document.createElement( 'option' );
786
+ option.setAttribute( 'value', id );
787
+ option.textContent = SPEAKER_LAYOUTS[ id ];
788
+ layoutDropdown.appendChild( option );
789
+ }
790
+
791
+ // Monitor the dropdown for changes
792
+ layoutDropdown.addEventListener( 'change', function( event ) {
793
+
794
+ setLayout( layoutDropdown.value );
795
+
796
+ }, false );
797
+
798
+ // Restore any currently persisted layout
799
+ setLayout( getLayout() );
800
+
801
+ }
802
+
803
+ /**
804
+ * Sets a new speaker view layout. The layout is persisted
805
+ * in local storage.
806
+ */
807
+ function setLayout( value ) {
808
+
809
+ var title = SPEAKER_LAYOUTS[ value ];
810
+
811
+ layoutLabel.innerHTML = 'Layout' + ( title ? ( ': ' + title ) : '' );
812
+ layoutDropdown.value = value;
813
+
814
+ document.body.setAttribute( 'data-speaker-layout', value );
815
+
816
+ // Persist locally
817
+ if( supportsLocalStorage() ) {
818
+ window.localStorage.setItem( 'reveal-speaker-layout', value );
819
+ }
820
+
821
+ }
822
+
823
+ /**
824
+ * Returns the ID of the most recently set speaker layout
825
+ * or our default layout if none has been set.
826
+ */
827
+ function getLayout() {
828
+
829
+ if( supportsLocalStorage() ) {
830
+ var layout = window.localStorage.getItem( 'reveal-speaker-layout' );
831
+ if( layout ) {
832
+ return layout;
833
+ }
834
+ }
835
+
836
+ // Default to the first record in the layouts hash
837
+ for( var id in SPEAKER_LAYOUTS ) {
838
+ return id;
839
+ }
840
+
841
+ }
842
+
843
+ function supportsLocalStorage() {
844
+
845
+ try {
846
+ localStorage.setItem('test', 'test');
847
+ localStorage.removeItem('test');
848
+ return true;
849
+ }
850
+ catch( e ) {
851
+ return false;
852
+ }
853
+
854
+ }
855
+
856
+ function zeroPadInteger( num ) {
857
+
858
+ var str = '00' + parseInt( num );
859
+ return str.substring( str.length - 2 );
860
+
861
+ }
862
+
863
+ /**
864
+ * Limits the frequency at which a function can be called.
865
+ */
866
+ function debounce( fn, ms ) {
867
+
868
+ var lastTime = 0,
869
+ timeout;
870
+
871
+ return function() {
872
+
873
+ var args = arguments;
874
+ var context = this;
875
+
876
+ clearTimeout( timeout );
877
+
878
+ var timeSinceLastCall = Date.now() - lastTime;
879
+ if( timeSinceLastCall > ms ) {
880
+ fn.apply( context, args );
881
+ lastTime = Date.now();
882
+ }
883
+ else {
884
+ timeout = setTimeout( function() {
885
+ fn.apply( context, args );
886
+ lastTime = Date.now();
887
+ }, ms - timeSinceLastCall );
888
+ }
889
+
890
+ }
891
+
892
+ }
893
+
894
+ })();
895
+
896
+ </script>
897
+ </body>
898
+ </html>