~tomasgroth/openlp/portable-path

« back to all changes in this revision

Viewing changes to openlp/core/display/html/reveal.js

  • Committer: Tomas Groth
  • Date: 2019-04-30 19:02:42 UTC
  • mfrom: (2829.2.32 openlp)
  • Revision ID: tomasgroth@yahoo.dk-20190430190242-6zwjk8724tyux70m
trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*!
 
2
 * reveal.js
 
3
 * http://revealjs.com
 
4
 * MIT licensed
 
5
 *
 
6
 * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
 
7
 */
 
8
(function( root, factory ) {
 
9
        if( typeof define === 'function' && define.amd ) {
 
10
                // AMD. Register as an anonymous module.
 
11
                define( function() {
 
12
                        root.Reveal = factory();
 
13
                        return root.Reveal;
 
14
                } );
 
15
        } else if( typeof exports === 'object' ) {
 
16
                // Node. Does not work with strict CommonJS.
 
17
                module.exports = factory();
 
18
        } else {
 
19
                // Browser globals.
 
20
                root.Reveal = factory();
 
21
        }
 
22
}( this, function() {
 
23
 
 
24
        'use strict';
 
25
 
 
26
        var Reveal;
 
27
 
 
28
        // The reveal.js version
 
29
        var VERSION = '3.8.0';
 
30
 
 
31
        var SLIDES_SELECTOR = '.slides section',
 
32
                HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
 
33
                VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
 
34
                HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
 
35
                UA = navigator.userAgent,
 
36
 
 
37
                // Configuration defaults, can be overridden at initialization time
 
38
                config = {
 
39
 
 
40
                        // The "normal" size of the presentation, aspect ratio will be preserved
 
41
                        // when the presentation is scaled to fit different resolutions
 
42
                        width: 960,
 
43
                        height: 700,
 
44
 
 
45
                        // Factor of the display size that should remain empty around the content
 
46
                        margin: 0.04,
 
47
 
 
48
                        // Bounds for smallest/largest possible scale to apply to content
 
49
                        minScale: 0.2,
 
50
                        maxScale: 2.0,
 
51
 
 
52
                        // Display presentation control arrows
 
53
                        controls: true,
 
54
 
 
55
                        // Help the user learn the controls by providing hints, for example by
 
56
                        // bouncing the down arrow when they first encounter a vertical slide
 
57
                        controlsTutorial: true,
 
58
 
 
59
                        // Determines where controls appear, "edges" or "bottom-right"
 
60
                        controlsLayout: 'bottom-right',
 
61
 
 
62
                        // Visibility rule for backwards navigation arrows; "faded", "hidden"
 
63
                        // or "visible"
 
64
                        controlsBackArrows: 'faded',
 
65
 
 
66
                        // Display a presentation progress bar
 
67
                        progress: true,
 
68
 
 
69
                        // Display the page number of the current slide
 
70
                        // - true:    Show slide number
 
71
                        // - false:   Hide slide number
 
72
                        //
 
73
                        // Can optionally be set as a string that specifies the number formatting:
 
74
                        // - "h.v":       Horizontal . vertical slide number (default)
 
75
                        // - "h/v":       Horizontal / vertical slide number
 
76
                        // - "c":         Flattened slide number
 
77
                        // - "c/t":       Flattened slide number / total slides
 
78
                        //
 
79
                        // Alternatively, you can provide a function that returns the slide
 
80
                        // number for the current slide. The function needs to return an array
 
81
                        // with one string [slideNumber] or three strings [n1,delimiter,n2].
 
82
                        // See #formatSlideNumber().
 
83
                        slideNumber: false,
 
84
 
 
85
                        // Can be used to limit the contexts in which the slide number appears
 
86
                        // - "all":      Always show the slide number
 
87
                        // - "print":    Only when printing to PDF
 
88
                        // - "speaker":  Only in the speaker view
 
89
                        showSlideNumber: 'all',
 
90
 
 
91
                        // Use 1 based indexing for # links to match slide number (default is zero
 
92
                        // based)
 
93
                        hashOneBasedIndex: false,
 
94
 
 
95
                        // Add the current slide number to the URL hash so that reloading the
 
96
                        // page/copying the URL will return you to the same slide
 
97
                        hash: false,
 
98
 
 
99
                        // Push each slide change to the browser history.  Implies `hash: true`
 
100
                        history: false,
 
101
 
 
102
                        // Enable keyboard shortcuts for navigation
 
103
                        keyboard: true,
 
104
 
 
105
                        // Optional function that blocks keyboard events when retuning false
 
106
                        keyboardCondition: null,
 
107
 
 
108
                        // Enable the slide overview mode
 
109
                        overview: true,
 
110
 
 
111
                        // Disables the default reveal.js slide layout so that you can use
 
112
                        // custom CSS layout
 
113
                        disableLayout: false,
 
114
 
 
115
                        // Vertical centering of slides
 
116
                        center: true,
 
117
 
 
118
                        // Enables touch navigation on devices with touch input
 
119
                        touch: true,
 
120
 
 
121
                        // Loop the presentation
 
122
                        loop: false,
 
123
 
 
124
                        // Change the presentation direction to be RTL
 
125
                        rtl: false,
 
126
 
 
127
                        // Changes the behavior of our navigation directions.
 
128
                        //
 
129
                        // "default"
 
130
                        // Left/right arrow keys step between horizontal slides, up/down
 
131
                        // arrow keys step between vertical slides. Space key steps through
 
132
                        // all slides (both horizontal and vertical).
 
133
                        //
 
134
                        // "linear"
 
135
                        // Removes the up/down arrows. Left/right arrows step through all
 
136
                        // slides (both horizontal and vertical).
 
137
                        //
 
138
                        // "grid"
 
139
                        // When this is enabled, stepping left/right from a vertical stack
 
140
                        // to an adjacent vertical stack will land you at the same vertical
 
141
                        // index.
 
142
                        //
 
143
                        // Consider a deck with six slides ordered in two vertical stacks:
 
144
                        // 1.1    2.1
 
145
                        // 1.2    2.2
 
146
                        // 1.3    2.3
 
147
                        //
 
148
                        // If you're on slide 1.3 and navigate right, you will normally move
 
149
                        // from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
 
150
                        // from 1.3 -> 2.3.
 
151
                        navigationMode: 'default',
 
152
 
 
153
                        // Randomizes the order of slides each time the presentation loads
 
154
                        shuffle: false,
 
155
 
 
156
                        // Turns fragments on and off globally
 
157
                        fragments: true,
 
158
 
 
159
                        // Flags whether to include the current fragment in the URL,
 
160
                        // so that reloading brings you to the same fragment position
 
161
                        fragmentInURL: false,
 
162
 
 
163
                        // Flags if the presentation is running in an embedded mode,
 
164
                        // i.e. contained within a limited portion of the screen
 
165
                        embedded: false,
 
166
 
 
167
                        // Flags if we should show a help overlay when the question-mark
 
168
                        // key is pressed
 
169
                        help: true,
 
170
 
 
171
                        // Flags if it should be possible to pause the presentation (blackout)
 
172
                        pause: true,
 
173
 
 
174
                        // Flags if speaker notes should be visible to all viewers
 
175
                        showNotes: false,
 
176
 
 
177
                        // Global override for autolaying embedded media (video/audio/iframe)
 
178
                        // - null:   Media will only autoplay if data-autoplay is present
 
179
                        // - true:   All media will autoplay, regardless of individual setting
 
180
                        // - false:  No media will autoplay, regardless of individual setting
 
181
                        autoPlayMedia: null,
 
182
 
 
183
                        // Global override for preloading lazy-loaded iframes
 
184
                        // - null:   Iframes with data-src AND data-preload will be loaded when within
 
185
                        //           the viewDistance, iframes with only data-src will be loaded when visible
 
186
                        // - true:   All iframes with data-src will be loaded when within the viewDistance
 
187
                        // - false:  All iframes with data-src will be loaded only when visible
 
188
                        preloadIframes: null,
 
189
 
 
190
                        // Controls automatic progression to the next slide
 
191
                        // - 0:      Auto-sliding only happens if the data-autoslide HTML attribute
 
192
                        //           is present on the current slide or fragment
 
193
                        // - 1+:     All slides will progress automatically at the given interval
 
194
                        // - false:  No auto-sliding, even if data-autoslide is present
 
195
                        autoSlide: 0,
 
196
 
 
197
                        // Stop auto-sliding after user input
 
198
                        autoSlideStoppable: true,
 
199
 
 
200
                        // Use this method for navigation when auto-sliding (defaults to navigateNext)
 
201
                        autoSlideMethod: null,
 
202
 
 
203
                        // Specify the average time in seconds that you think you will spend
 
204
                        // presenting each slide. This is used to show a pacing timer in the
 
205
                        // speaker view
 
206
                        defaultTiming: null,
 
207
 
 
208
                        // Enable slide navigation via mouse wheel
 
209
                        mouseWheel: false,
 
210
 
 
211
                        // Apply a 3D roll to links on hover
 
212
                        rollingLinks: false,
 
213
 
 
214
                        // Hides the address bar on mobile devices
 
215
                        hideAddressBar: true,
 
216
 
 
217
                        // Opens links in an iframe preview overlay
 
218
                        // Add `data-preview-link` and `data-preview-link="false"` to customise each link
 
219
                        // individually
 
220
                        previewLinks: false,
 
221
 
 
222
                        // Exposes the reveal.js API through window.postMessage
 
223
                        postMessage: true,
 
224
 
 
225
                        // Dispatches all reveal.js events to the parent window through postMessage
 
226
                        postMessageEvents: false,
 
227
 
 
228
                        // Focuses body when page changes visibility to ensure keyboard shortcuts work
 
229
                        focusBodyOnPageVisibilityChange: true,
 
230
 
 
231
                        // Transition style
 
232
                        transition: 'slide', // none/fade/slide/convex/concave/zoom
 
233
 
 
234
                        // Transition speed
 
235
                        transitionSpeed: 'default', // default/fast/slow
 
236
 
 
237
                        // Transition style for full page slide backgrounds
 
238
                        backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
 
239
 
 
240
                        // Parallax background image
 
241
                        parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
 
242
 
 
243
                        // Parallax background size
 
244
                        parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
 
245
 
 
246
                        // Parallax background repeat
 
247
                        parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
 
248
 
 
249
                        // Parallax background position
 
250
                        parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
 
251
 
 
252
                        // Amount of pixels to move the parallax background per slide step
 
253
                        parallaxBackgroundHorizontal: null,
 
254
                        parallaxBackgroundVertical: null,
 
255
 
 
256
                        // The maximum number of pages a single slide can expand onto when printing
 
257
                        // to PDF, unlimited by default
 
258
                        pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
 
259
 
 
260
                        // Prints each fragment on a separate slide
 
261
                        pdfSeparateFragments: true,
 
262
 
 
263
                        // Offset used to reduce the height of content within exported PDF pages.
 
264
                        // This exists to account for environment differences based on how you
 
265
                        // print to PDF. CLI printing options, like phantomjs and wkpdf, can end
 
266
                        // on precisely the total height of the document whereas in-browser
 
267
                        // printing has to end one pixel before.
 
268
                        pdfPageHeightOffset: -1,
 
269
 
 
270
                        // Number of slides away from the current that are visible
 
271
                        viewDistance: 3,
 
272
 
 
273
                        // The display mode that will be used to show slides
 
274
                        display: 'block',
 
275
 
 
276
                        // Hide cursor if inactive
 
277
                        hideInactiveCursor: true,
 
278
 
 
279
                        // Time before the cursor is hidden (in ms)
 
280
                        hideCursorTime: 5000,
 
281
 
 
282
                        // Script dependencies to load
 
283
                        dependencies: []
 
284
 
 
285
                },
 
286
 
 
287
                // Flags if Reveal.initialize() has been called
 
288
                initialized = false,
 
289
 
 
290
                // Flags if reveal.js is loaded (has dispatched the 'ready' event)
 
291
                loaded = false,
 
292
 
 
293
                // Flags if the overview mode is currently active
 
294
                overview = false,
 
295
 
 
296
                // Holds the dimensions of our overview slides, including margins
 
297
                overviewSlideWidth = null,
 
298
                overviewSlideHeight = null,
 
299
 
 
300
                // The horizontal and vertical index of the currently active slide
 
301
                indexh,
 
302
                indexv,
 
303
 
 
304
                // The previous and current slide HTML elements
 
305
                previousSlide,
 
306
                currentSlide,
 
307
 
 
308
                previousBackground,
 
309
 
 
310
                // Remember which directions that the user has navigated towards
 
311
                hasNavigatedRight = false,
 
312
                hasNavigatedDown = false,
 
313
 
 
314
                // Slides may hold a data-state attribute which we pick up and apply
 
315
                // as a class to the body. This list contains the combined state of
 
316
                // all current slides.
 
317
                state = [],
 
318
 
 
319
                // The current scale of the presentation (see width/height config)
 
320
                scale = 1,
 
321
 
 
322
                // CSS transform that is currently applied to the slides container,
 
323
                // split into two groups
 
324
                slidesTransform = { layout: '', overview: '' },
 
325
 
 
326
                // Cached references to DOM elements
 
327
                dom = {},
 
328
 
 
329
                // A list of registered reveal.js plugins
 
330
                plugins = {},
 
331
 
 
332
                // List of asynchronously loaded reveal.js dependencies
 
333
                asyncDependencies = [],
 
334
 
 
335
                // Features supported by the browser, see #checkCapabilities()
 
336
                features = {},
 
337
 
 
338
                // Client is a mobile device, see #checkCapabilities()
 
339
                isMobileDevice,
 
340
 
 
341
                // Client is a desktop Chrome, see #checkCapabilities()
 
342
                isChrome,
 
343
 
 
344
                // Throttles mouse wheel navigation
 
345
                lastMouseWheelStep = 0,
 
346
 
 
347
                // Delays updates to the URL due to a Chrome thumbnailer bug
 
348
                writeURLTimeout = 0,
 
349
 
 
350
                // Is the mouse pointer currently hidden from view
 
351
                cursorHidden = false,
 
352
 
 
353
                // Timeout used to determine when the cursor is inactive
 
354
                cursorInactiveTimeout = 0,
 
355
 
 
356
                // Flags if the interaction event listeners are bound
 
357
                eventsAreBound = false,
 
358
 
 
359
                // The current auto-slide duration
 
360
                autoSlide = 0,
 
361
 
 
362
                // Auto slide properties
 
363
                autoSlidePlayer,
 
364
                autoSlideTimeout = 0,
 
365
                autoSlideStartTime = -1,
 
366
                autoSlidePaused = false,
 
367
 
 
368
                // Holds information about the currently ongoing touch input
 
369
                touch = {
 
370
                        startX: 0,
 
371
                        startY: 0,
 
372
                        startCount: 0,
 
373
                        captured: false,
 
374
                        threshold: 40
 
375
                },
 
376
 
 
377
                // A key:value map of shortcut keyboard keys and descriptions of
 
378
                // the actions they trigger, generated in #configure()
 
379
                keyboardShortcuts = {},
 
380
 
 
381
                // Holds custom key code mappings
 
382
                registeredKeyBindings = {};
 
383
 
 
384
        /**
 
385
         * Starts up the presentation if the client is capable.
 
386
         */
 
387
        function initialize( options ) {
 
388
 
 
389
                // Make sure we only initialize once
 
390
                if( initialized === true ) return;
 
391
 
 
392
                initialized = true;
 
393
 
 
394
                checkCapabilities();
 
395
 
 
396
                if( !features.transforms2d && !features.transforms3d ) {
 
397
                        document.body.setAttribute( 'class', 'no-transforms' );
 
398
 
 
399
                        // Since JS won't be running any further, we load all lazy
 
400
                        // loading elements upfront
 
401
                        var images = toArray( document.getElementsByTagName( 'img' ) ),
 
402
                                iframes = toArray( document.getElementsByTagName( 'iframe' ) );
 
403
 
 
404
                        var lazyLoadable = images.concat( iframes );
 
405
 
 
406
                        for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
 
407
                                var element = lazyLoadable[i];
 
408
                                if( element.getAttribute( 'data-src' ) ) {
 
409
                                        element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
 
410
                                        element.removeAttribute( 'data-src' );
 
411
                                }
 
412
                        }
 
413
 
 
414
                        // If the browser doesn't support core features we won't be
 
415
                        // using JavaScript to control the presentation
 
416
                        return;
 
417
                }
 
418
 
 
419
                // Cache references to key DOM elements
 
420
                dom.wrapper = document.querySelector( '.reveal' );
 
421
                dom.slides = document.querySelector( '.reveal .slides' );
 
422
 
 
423
                // Force a layout when the whole page, incl fonts, has loaded
 
424
                window.addEventListener( 'load', layout, false );
 
425
 
 
426
                var query = Reveal.getQueryHash();
 
427
 
 
428
                // Do not accept new dependencies via query config to avoid
 
429
                // the potential of malicious script injection
 
430
                if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
 
431
 
 
432
                // Copy options over to our config object
 
433
                extend( config, options );
 
434
                extend( config, query );
 
435
 
 
436
                // Hide the address bar in mobile browsers
 
437
                hideAddressBar();
 
438
 
 
439
                // Loads dependencies and continues to #start() once done
 
440
                load();
 
441
 
 
442
        }
 
443
 
 
444
        /**
 
445
         * Restarts up the presentation if the client is capable.
 
446
         */
 
447
    function reinitialize() {
 
448
        initialized = false;
 
449
        initialize(config);
 
450
    }
 
451
 
 
452
        /**
 
453
         * Inspect the client to see what it's capable of, this
 
454
         * should only happens once per runtime.
 
455
         */
 
456
        function checkCapabilities() {
 
457
 
 
458
                isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
 
459
                isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
 
460
 
 
461
                var testElement = document.createElement( 'div' );
 
462
 
 
463
                features.transforms3d = 'WebkitPerspective' in testElement.style ||
 
464
                                                                'MozPerspective' in testElement.style ||
 
465
                                                                'msPerspective' in testElement.style ||
 
466
                                                                'OPerspective' in testElement.style ||
 
467
                                                                'perspective' in testElement.style;
 
468
 
 
469
                features.transforms2d = 'WebkitTransform' in testElement.style ||
 
470
                                                                'MozTransform' in testElement.style ||
 
471
                                                                'msTransform' in testElement.style ||
 
472
                                                                'OTransform' in testElement.style ||
 
473
                                                                'transform' in testElement.style;
 
474
 
 
475
                features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
 
476
                features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
 
477
 
 
478
                features.canvas = !!document.createElement( 'canvas' ).getContext;
 
479
 
 
480
                // Transitions in the overview are disabled in desktop and
 
481
                // Safari due to lag
 
482
                features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
 
483
 
 
484
                // Flags if we should use zoom instead of transform to scale
 
485
                // up slides. Zoom produces crisper results but has a lot of
 
486
                // xbrowser quirks so we only use it in whitelsited browsers.
 
487
                features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
 
488
                                                ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
 
489
 
 
490
        }
 
491
 
 
492
        /**
 
493
         * Loads the dependencies of reveal.js. Dependencies are
 
494
         * defined via the configuration option 'dependencies'
 
495
         * and will be loaded prior to starting/binding reveal.js.
 
496
         * Some dependencies may have an 'async' flag, if so they
 
497
         * will load after reveal.js has been started up.
 
498
         */
 
499
        function load() {
 
500
 
 
501
                var scripts = [],
 
502
                        scriptsToLoad = 0;
 
503
 
 
504
                config.dependencies.forEach( function( s ) {
 
505
                        // Load if there's no condition or the condition is truthy
 
506
                        if( !s.condition || s.condition() ) {
 
507
                                if( s.async ) {
 
508
                                        asyncDependencies.push( s );
 
509
                                }
 
510
                                else {
 
511
                                        scripts.push( s );
 
512
                                }
 
513
                        }
 
514
                } );
 
515
 
 
516
                if( scripts.length ) {
 
517
                        scriptsToLoad = scripts.length;
 
518
 
 
519
                        // Load synchronous scripts
 
520
                        scripts.forEach( function( s ) {
 
521
                                loadScript( s.src, function() {
 
522
 
 
523
                                        if( typeof s.callback === 'function' ) s.callback();
 
524
 
 
525
                                        if( --scriptsToLoad === 0 ) {
 
526
                                                initPlugins();
 
527
                                        }
 
528
 
 
529
                                } );
 
530
                        } );
 
531
                }
 
532
                else {
 
533
                        initPlugins();
 
534
                }
 
535
 
 
536
        }
 
537
 
 
538
        /**
 
539
         * Initializes our plugins and waits for them to be ready
 
540
         * before proceeding.
 
541
         */
 
542
        function initPlugins() {
 
543
 
 
544
                var pluginsToInitialize = Object.keys( plugins ).length;
 
545
 
 
546
                // If there are no plugins, skip this step
 
547
                if( pluginsToInitialize === 0 ) {
 
548
                        loadAsyncDependencies();
 
549
                }
 
550
                // ... otherwise initialize plugins
 
551
                else {
 
552
 
 
553
                        var afterPlugInitialized = function() {
 
554
                                if( --pluginsToInitialize === 0 ) {
 
555
                                        loadAsyncDependencies();
 
556
                                }
 
557
                        };
 
558
 
 
559
                        for( var i in plugins ) {
 
560
 
 
561
                                var plugin = plugins[i];
 
562
 
 
563
                                // If the plugin has an 'init' method, invoke it
 
564
                                if( typeof plugin.init === 'function' ) {
 
565
                                        var callback = plugin.init();
 
566
 
 
567
                                        // If the plugin returned a Promise, wait for it
 
568
                                        if( callback && typeof callback.then === 'function' ) {
 
569
                                                callback.then( afterPlugInitialized );
 
570
                                        }
 
571
                                        else {
 
572
                                                afterPlugInitialized();
 
573
                                        }
 
574
                                }
 
575
                                else {
 
576
                                        afterPlugInitialized();
 
577
                                }
 
578
 
 
579
                        }
 
580
 
 
581
                }
 
582
 
 
583
        }
 
584
 
 
585
        /**
 
586
         * Loads all async reveal.js dependencies.
 
587
         */
 
588
        function loadAsyncDependencies() {
 
589
 
 
590
                if( asyncDependencies.length ) {
 
591
                        asyncDependencies.forEach( function( s ) {
 
592
                                loadScript( s.src, s.callback );
 
593
                        } );
 
594
                }
 
595
 
 
596
                start();
 
597
 
 
598
        }
 
599
 
 
600
        /**
 
601
         * Loads a JavaScript file from the given URL and executes it.
 
602
         *
 
603
         * @param {string} url Address of the .js file to load
 
604
         * @param {function} callback Method to invoke when the script
 
605
         * has loaded and executed
 
606
         */
 
607
        function loadScript( url, callback ) {
 
608
 
 
609
                var script = document.createElement( 'script' );
 
610
                script.type = 'text/javascript';
 
611
                script.async = false;
 
612
                script.defer = false;
 
613
                script.src = url;
 
614
 
 
615
                if( callback ) {
 
616
 
 
617
                        // Success callback
 
618
                        script.onload = script.onreadystatechange = function( event ) {
 
619
                                if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
 
620
 
 
621
                                        // Kill event listeners
 
622
                                        script.onload = script.onreadystatechange = script.onerror = null;
 
623
 
 
624
                                        callback();
 
625
 
 
626
                                }
 
627
                        };
 
628
 
 
629
                        // Error callback
 
630
                        script.onerror = function( err ) {
 
631
 
 
632
                                // Kill event listeners
 
633
                                script.onload = script.onreadystatechange = script.onerror = null;
 
634
 
 
635
                                callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
 
636
 
 
637
                        };
 
638
 
 
639
                }
 
640
 
 
641
                // Append the script at the end of <head>
 
642
                var head = document.querySelector( 'head' );
 
643
                head.insertBefore( script, head.lastChild );
 
644
 
 
645
        }
 
646
 
 
647
        /**
 
648
         * Starts up reveal.js by binding input events and navigating
 
649
         * to the current URL deeplink if there is one.
 
650
         */
 
651
        function start() {
 
652
 
 
653
                loaded = true;
 
654
 
 
655
                // Make sure we've got all the DOM elements we need
 
656
                setupDOM();
 
657
 
 
658
                // Listen to messages posted to this window
 
659
                setupPostMessage();
 
660
 
 
661
                // Prevent the slides from being scrolled out of view
 
662
                setupScrollPrevention();
 
663
 
 
664
                // Resets all vertical slides so that only the first is visible
 
665
                resetVerticalSlides();
 
666
 
 
667
                // Updates the presentation to match the current configuration values
 
668
                configure();
 
669
 
 
670
                // Read the initial hash
 
671
                readURL();
 
672
 
 
673
                // Update all backgrounds
 
674
                updateBackground( true );
 
675
 
 
676
                // Notify listeners that the presentation is ready but use a 1ms
 
677
                // timeout to ensure it's not fired synchronously after #initialize()
 
678
                setTimeout( function() {
 
679
                        // Enable transitions now that we're loaded
 
680
                        dom.slides.classList.remove( 'no-transition' );
 
681
 
 
682
                        dom.wrapper.classList.add( 'ready' );
 
683
 
 
684
                        dispatchEvent( 'ready', {
 
685
                                'indexh': indexh,
 
686
                                'indexv': indexv,
 
687
                                'currentSlide': currentSlide
 
688
                        } );
 
689
                }, 1 );
 
690
 
 
691
                // Special setup and config is required when printing to PDF
 
692
                if( isPrintingPDF() ) {
 
693
                        removeEventListeners();
 
694
 
 
695
                        // The document needs to have loaded for the PDF layout
 
696
                        // measurements to be accurate
 
697
                        if( document.readyState === 'complete' ) {
 
698
                                setupPDF();
 
699
                        }
 
700
                        else {
 
701
                                window.addEventListener( 'load', setupPDF );
 
702
                        }
 
703
                }
 
704
 
 
705
        }
 
706
 
 
707
        /**
 
708
         * Finds and stores references to DOM elements which are
 
709
         * required by the presentation. If a required element is
 
710
         * not found, it is created.
 
711
         */
 
712
        function setupDOM() {
 
713
 
 
714
                // Prevent transitions while we're loading
 
715
                dom.slides.classList.add( 'no-transition' );
 
716
 
 
717
                if( isMobileDevice ) {
 
718
                        dom.wrapper.classList.add( 'no-hover' );
 
719
                }
 
720
                else {
 
721
                        dom.wrapper.classList.remove( 'no-hover' );
 
722
                }
 
723
 
 
724
                if( /iphone/gi.test( UA ) ) {
 
725
                        dom.wrapper.classList.add( 'ua-iphone' );
 
726
                }
 
727
                else {
 
728
                        dom.wrapper.classList.remove( 'ua-iphone' );
 
729
                }
 
730
 
 
731
                // Background element
 
732
                dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
 
733
 
 
734
                // Progress bar
 
735
                dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
 
736
                dom.progressbar = dom.progress.querySelector( 'span' );
 
737
 
 
738
                // Arrow controls
 
739
                dom.controls = createSingletonNode( dom.wrapper, 'aside', 'controls',
 
740
                        '<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' +
 
741
                        '<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' +
 
742
                        '<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' +
 
743
                        '<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>' );
 
744
 
 
745
                // Slide number
 
746
                dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
 
747
 
 
748
                // Element containing notes that are visible to the audience
 
749
                dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
 
750
                dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
 
751
                dom.speakerNotes.setAttribute( 'tabindex', '0' );
 
752
 
 
753
                // Overlay graphic which is displayed during the paused mode
 
754
                dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
 
755
 
 
756
                dom.wrapper.setAttribute( 'role', 'application' );
 
757
 
 
758
                // There can be multiple instances of controls throughout the page
 
759
                dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
 
760
                dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
 
761
                dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
 
762
                dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
 
763
                dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
 
764
                dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
 
765
 
 
766
                // The right and down arrows in the standard reveal.js controls
 
767
                dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' );
 
768
                dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' );
 
769
 
 
770
                dom.statusDiv = createStatusDiv();
 
771
        }
 
772
 
 
773
        /**
 
774
         * Creates a hidden div with role aria-live to announce the
 
775
         * current slide content. Hide the div off-screen to make it
 
776
         * available only to Assistive Technologies.
 
777
         *
 
778
         * @return {HTMLElement}
 
779
         */
 
780
        function createStatusDiv() {
 
781
 
 
782
                var statusDiv = document.getElementById( 'aria-status-div' );
 
783
                if( !statusDiv ) {
 
784
                        statusDiv = document.createElement( 'div' );
 
785
                        statusDiv.style.position = 'absolute';
 
786
                        statusDiv.style.height = '1px';
 
787
                        statusDiv.style.width = '1px';
 
788
                        statusDiv.style.overflow = 'hidden';
 
789
                        statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
 
790
                        statusDiv.setAttribute( 'id', 'aria-status-div' );
 
791
                        statusDiv.setAttribute( 'aria-live', 'polite' );
 
792
                        statusDiv.setAttribute( 'aria-atomic','true' );
 
793
                        dom.wrapper.appendChild( statusDiv );
 
794
                }
 
795
                return statusDiv;
 
796
 
 
797
        }
 
798
 
 
799
        /**
 
800
         * Converts the given HTML element into a string of text
 
801
         * that can be announced to a screen reader. Hidden
 
802
         * elements are excluded.
 
803
         */
 
804
        function getStatusText( node ) {
 
805
 
 
806
                var text = '';
 
807
 
 
808
                // Text node
 
809
                if( node.nodeType === 3 ) {
 
810
                        text += node.textContent;
 
811
                }
 
812
                // Element node
 
813
                else if( node.nodeType === 1 ) {
 
814
 
 
815
                        var isAriaHidden = node.getAttribute( 'aria-hidden' );
 
816
                        var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
 
817
                        if( isAriaHidden !== 'true' && !isDisplayHidden ) {
 
818
 
 
819
                                toArray( node.childNodes ).forEach( function( child ) {
 
820
                                        text += getStatusText( child );
 
821
                                } );
 
822
 
 
823
                        }
 
824
 
 
825
                }
 
826
 
 
827
                return text;
 
828
 
 
829
        }
 
830
 
 
831
        /**
 
832
         * Configures the presentation for printing to a static
 
833
         * PDF.
 
834
         */
 
835
        function setupPDF() {
 
836
 
 
837
                var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
 
838
 
 
839
                // Dimensions of the PDF pages
 
840
                var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
 
841
                        pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
 
842
 
 
843
                // Dimensions of slides within the pages
 
844
                var slideWidth = slideSize.width,
 
845
                        slideHeight = slideSize.height;
 
846
 
 
847
                // Let the browser know what page size we want to print
 
848
                injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
 
849
 
 
850
                // Limit the size of certain elements to the dimensions of the slide
 
851
                injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
 
852
 
 
853
                document.body.classList.add( 'print-pdf' );
 
854
                document.body.style.width = pageWidth + 'px';
 
855
                document.body.style.height = pageHeight + 'px';
 
856
 
 
857
                // Make sure stretch elements fit on slide
 
858
                layoutSlideContents( slideWidth, slideHeight );
 
859
 
 
860
                // Add each slide's index as attributes on itself, we need these
 
861
                // indices to generate slide numbers below
 
862
                toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
 
863
                        hslide.setAttribute( 'data-index-h', h );
 
864
 
 
865
                        if( hslide.classList.contains( 'stack' ) ) {
 
866
                                toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
 
867
                                        vslide.setAttribute( 'data-index-h', h );
 
868
                                        vslide.setAttribute( 'data-index-v', v );
 
869
                                } );
 
870
                        }
 
871
                } );
 
872
 
 
873
                // Slide and slide background layout
 
874
                toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
 
875
 
 
876
                        // Vertical stacks are not centred since their section
 
877
                        // children will be
 
878
                        if( slide.classList.contains( 'stack' ) === false ) {
 
879
                                // Center the slide inside of the page, giving the slide some margin
 
880
                                var left = ( pageWidth - slideWidth ) / 2,
 
881
                                        top = ( pageHeight - slideHeight ) / 2;
 
882
 
 
883
                                var contentHeight = slide.scrollHeight;
 
884
                                var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
 
885
 
 
886
                                // Adhere to configured pages per slide limit
 
887
                                numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
 
888
 
 
889
                                // Center slides vertically
 
890
                                if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
 
891
                                        top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
 
892
                                }
 
893
 
 
894
                                // Wrap the slide in a page element and hide its overflow
 
895
                                // so that no page ever flows onto another
 
896
                                var page = document.createElement( 'div' );
 
897
                                page.className = 'pdf-page';
 
898
                                page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
 
899
                                slide.parentNode.insertBefore( page, slide );
 
900
                                page.appendChild( slide );
 
901
 
 
902
                                // Position the slide inside of the page
 
903
                                slide.style.left = left + 'px';
 
904
                                slide.style.top = top + 'px';
 
905
                                slide.style.width = slideWidth + 'px';
 
906
 
 
907
                                if( slide.slideBackgroundElement ) {
 
908
                                        page.insertBefore( slide.slideBackgroundElement, slide );
 
909
                                }
 
910
 
 
911
                                // Inject notes if `showNotes` is enabled
 
912
                                if( config.showNotes ) {
 
913
 
 
914
                                        // Are there notes for this slide?
 
915
                                        var notes = getSlideNotes( slide );
 
916
                                        if( notes ) {
 
917
 
 
918
                                                var notesSpacing = 8;
 
919
                                                var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
 
920
                                                var notesElement = document.createElement( 'div' );
 
921
                                                notesElement.classList.add( 'speaker-notes' );
 
922
                                                notesElement.classList.add( 'speaker-notes-pdf' );
 
923
                                                notesElement.setAttribute( 'data-layout', notesLayout );
 
924
                                                notesElement.innerHTML = notes;
 
925
 
 
926
                                                if( notesLayout === 'separate-page' ) {
 
927
                                                        page.parentNode.insertBefore( notesElement, page.nextSibling );
 
928
                                                }
 
929
                                                else {
 
930
                                                        notesElement.style.left = notesSpacing + 'px';
 
931
                                                        notesElement.style.bottom = notesSpacing + 'px';
 
932
                                                        notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
 
933
                                                        page.appendChild( notesElement );
 
934
                                                }
 
935
 
 
936
                                        }
 
937
 
 
938
                                }
 
939
 
 
940
                                // Inject slide numbers if `slideNumbers` are enabled
 
941
                                if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
 
942
                                        var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
 
943
                                                slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
 
944
 
 
945
                                        var numberElement = document.createElement( 'div' );
 
946
                                        numberElement.classList.add( 'slide-number' );
 
947
                                        numberElement.classList.add( 'slide-number-pdf' );
 
948
                                        numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
 
949
                                        page.appendChild( numberElement );
 
950
                                }
 
951
 
 
952
                                // Copy page and show fragments one after another
 
953
                                if( config.pdfSeparateFragments ) {
 
954
 
 
955
                                        // Each fragment 'group' is an array containing one or more
 
956
                                        // fragments. Multiple fragments that appear at the same time
 
957
                                        // are part of the same group.
 
958
                                        var fragmentGroups = sortFragments( page.querySelectorAll( '.fragment' ), true );
 
959
 
 
960
                                        var previousFragmentStep;
 
961
                                        var previousPage;
 
962
 
 
963
                                        fragmentGroups.forEach( function( fragments ) {
 
964
 
 
965
                                                // Remove 'current-fragment' from the previous group
 
966
                                                if( previousFragmentStep ) {
 
967
                                                        previousFragmentStep.forEach( function( fragment ) {
 
968
                                                                fragment.classList.remove( 'current-fragment' );
 
969
                                                        } );
 
970
                                                }
 
971
 
 
972
                                                // Show the fragments for the current index
 
973
                                                fragments.forEach( function( fragment ) {
 
974
                                                        fragment.classList.add( 'visible', 'current-fragment' );
 
975
                                                } );
 
976
 
 
977
                                                // Create a separate page for the current fragment state
 
978
                                                var clonedPage = page.cloneNode( true );
 
979
                                                page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling );
 
980
 
 
981
                                                previousFragmentStep = fragments;
 
982
                                                previousPage = clonedPage;
 
983
 
 
984
                                        } );
 
985
 
 
986
                                        // Reset the first/original page so that all fragments are hidden
 
987
                                        fragmentGroups.forEach( function( fragments ) {
 
988
                                                fragments.forEach( function( fragment ) {
 
989
                                                        fragment.classList.remove( 'visible', 'current-fragment' );
 
990
                                                } );
 
991
                                        } );
 
992
 
 
993
                                }
 
994
                                // Show all fragments
 
995
                                else {
 
996
                                        toArray( page.querySelectorAll( '.fragment:not(.fade-out)' ) ).forEach( function( fragment ) {
 
997
                                                fragment.classList.add( 'visible' );
 
998
                                        } );
 
999
                                }
 
1000
 
 
1001
                        }
 
1002
 
 
1003
                } );
 
1004
 
 
1005
                // Notify subscribers that the PDF layout is good to go
 
1006
                dispatchEvent( 'pdf-ready' );
 
1007
 
 
1008
        }
 
1009
 
 
1010
        /**
 
1011
         * This is an unfortunate necessity. Some actions â€“ such as
 
1012
         * an input field being focused in an iframe or using the
 
1013
         * keyboard to expand text selection beyond the bounds of
 
1014
         * a slide â€“ can trigger our content to be pushed out of view.
 
1015
         * This scrolling can not be prevented by hiding overflow in
 
1016
         * CSS (we already do) so we have to resort to repeatedly
 
1017
         * checking if the slides have been offset :(
 
1018
         */
 
1019
        function setupScrollPrevention() {
 
1020
 
 
1021
                setInterval( function() {
 
1022
                        if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
 
1023
                                dom.wrapper.scrollTop = 0;
 
1024
                                dom.wrapper.scrollLeft = 0;
 
1025
                        }
 
1026
                }, 1000 );
 
1027
 
 
1028
        }
 
1029
 
 
1030
        /**
 
1031
         * Creates an HTML element and returns a reference to it.
 
1032
         * If the element already exists the existing instance will
 
1033
         * be returned.
 
1034
         *
 
1035
         * @param {HTMLElement} container
 
1036
         * @param {string} tagname
 
1037
         * @param {string} classname
 
1038
         * @param {string} innerHTML
 
1039
         *
 
1040
         * @return {HTMLElement}
 
1041
         */
 
1042
        function createSingletonNode( container, tagname, classname, innerHTML ) {
 
1043
 
 
1044
                // Find all nodes matching the description
 
1045
                var nodes = container.querySelectorAll( '.' + classname );
 
1046
 
 
1047
                // Check all matches to find one which is a direct child of
 
1048
                // the specified container
 
1049
                for( var i = 0; i < nodes.length; i++ ) {
 
1050
                        var testNode = nodes[i];
 
1051
                        if( testNode.parentNode === container ) {
 
1052
                                return testNode;
 
1053
                        }
 
1054
                }
 
1055
 
 
1056
                // If no node was found, create it now
 
1057
                var node = document.createElement( tagname );
 
1058
                node.className = classname;
 
1059
                if( typeof innerHTML === 'string' ) {
 
1060
                        node.innerHTML = innerHTML;
 
1061
                }
 
1062
                container.appendChild( node );
 
1063
 
 
1064
                return node;
 
1065
 
 
1066
        }
 
1067
 
 
1068
        /**
 
1069
         * Creates the slide background elements and appends them
 
1070
         * to the background container. One element is created per
 
1071
         * slide no matter if the given slide has visible background.
 
1072
         */
 
1073
        function createBackgrounds() {
 
1074
 
 
1075
                var printMode = isPrintingPDF();
 
1076
 
 
1077
                // Clear prior backgrounds
 
1078
                dom.background.innerHTML = '';
 
1079
                dom.background.classList.add( 'no-transition' );
 
1080
 
 
1081
                // Iterate over all horizontal slides
 
1082
                toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
 
1083
 
 
1084
                        var backgroundStack = createBackground( slideh, dom.background );
 
1085
 
 
1086
                        // Iterate over all vertical slides
 
1087
                        toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
 
1088
 
 
1089
                                createBackground( slidev, backgroundStack );
 
1090
 
 
1091
                                backgroundStack.classList.add( 'stack' );
 
1092
 
 
1093
                        } );
 
1094
 
 
1095
                } );
 
1096
 
 
1097
                // Add parallax background if specified
 
1098
                if( config.parallaxBackgroundImage ) {
 
1099
 
 
1100
                        dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
 
1101
                        dom.background.style.backgroundSize = config.parallaxBackgroundSize;
 
1102
                        dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat;
 
1103
                        dom.background.style.backgroundPosition = config.parallaxBackgroundPosition;
 
1104
 
 
1105
                        // Make sure the below properties are set on the element - these properties are
 
1106
                        // needed for proper transitions to be set on the element via CSS. To remove
 
1107
                        // annoying background slide-in effect when the presentation starts, apply
 
1108
                        // these properties after short time delay
 
1109
                        setTimeout( function() {
 
1110
                                dom.wrapper.classList.add( 'has-parallax-background' );
 
1111
                        }, 1 );
 
1112
 
 
1113
                }
 
1114
                else {
 
1115
 
 
1116
                        dom.background.style.backgroundImage = '';
 
1117
                        dom.wrapper.classList.remove( 'has-parallax-background' );
 
1118
 
 
1119
                }
 
1120
 
 
1121
        }
 
1122
 
 
1123
        /**
 
1124
         * Creates a background for the given slide.
 
1125
         *
 
1126
         * @param {HTMLElement} slide
 
1127
         * @param {HTMLElement} container The element that the background
 
1128
         * should be appended to
 
1129
         * @return {HTMLElement} New background div
 
1130
         */
 
1131
        function createBackground( slide, container ) {
 
1132
 
 
1133
 
 
1134
                // Main slide background element
 
1135
                var element = document.createElement( 'div' );
 
1136
                element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
 
1137
 
 
1138
                // Inner background element that wraps images/videos/iframes
 
1139
                var contentElement = document.createElement( 'div' );
 
1140
                contentElement.className = 'slide-background-content';
 
1141
 
 
1142
                element.appendChild( contentElement );
 
1143
                container.appendChild( element );
 
1144
 
 
1145
                slide.slideBackgroundElement = element;
 
1146
                slide.slideBackgroundContentElement = contentElement;
 
1147
 
 
1148
                // Syncs the background to reflect all current background settings
 
1149
                syncBackground( slide );
 
1150
 
 
1151
                return element;
 
1152
 
 
1153
        }
 
1154
 
 
1155
        /**
 
1156
         * Renders all of the visual properties of a slide background
 
1157
         * based on the various background attributes.
 
1158
         *
 
1159
         * @param {HTMLElement} slide
 
1160
         */
 
1161
        function syncBackground( slide ) {
 
1162
 
 
1163
                var element = slide.slideBackgroundElement,
 
1164
                        contentElement = slide.slideBackgroundContentElement;
 
1165
 
 
1166
                // Reset the prior background state in case this is not the
 
1167
                // initial sync
 
1168
                slide.classList.remove( 'has-dark-background' );
 
1169
                slide.classList.remove( 'has-light-background' );
 
1170
 
 
1171
                element.removeAttribute( 'data-loaded' );
 
1172
                element.removeAttribute( 'data-background-hash' );
 
1173
                element.removeAttribute( 'data-background-size' );
 
1174
                element.removeAttribute( 'data-background-transition' );
 
1175
                element.style.backgroundColor = '';
 
1176
 
 
1177
                contentElement.style.backgroundSize = '';
 
1178
                contentElement.style.backgroundRepeat = '';
 
1179
                contentElement.style.backgroundPosition = '';
 
1180
                contentElement.style.backgroundImage = '';
 
1181
                contentElement.style.opacity = '';
 
1182
                contentElement.innerHTML = '';
 
1183
 
 
1184
                var data = {
 
1185
                        background: slide.getAttribute( 'data-background' ),
 
1186
                        backgroundSize: slide.getAttribute( 'data-background-size' ),
 
1187
                        backgroundImage: slide.getAttribute( 'data-background-image' ),
 
1188
                        backgroundVideo: slide.getAttribute( 'data-background-video' ),
 
1189
                        backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
 
1190
                        backgroundColor: slide.getAttribute( 'data-background-color' ),
 
1191
                        backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
 
1192
                        backgroundPosition: slide.getAttribute( 'data-background-position' ),
 
1193
                        backgroundTransition: slide.getAttribute( 'data-background-transition' ),
 
1194
                        backgroundOpacity: slide.getAttribute( 'data-background-opacity' )
 
1195
                };
 
1196
 
 
1197
                if( data.background ) {
 
1198
                        // Auto-wrap image urls in url(...)
 
1199
                        if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) {
 
1200
                                slide.setAttribute( 'data-background-image', data.background );
 
1201
                        }
 
1202
                        else {
 
1203
                                element.style.background = data.background;
 
1204
                        }
 
1205
                }
 
1206
 
 
1207
                // Create a hash for this combination of background settings.
 
1208
                // This is used to determine when two slide backgrounds are
 
1209
                // the same.
 
1210
                if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
 
1211
                        element.setAttribute( 'data-background-hash', data.background +
 
1212
                                                                                                                        data.backgroundSize +
 
1213
                                                                                                                        data.backgroundImage +
 
1214
                                                                                                                        data.backgroundVideo +
 
1215
                                                                                                                        data.backgroundIframe +
 
1216
                                                                                                                        data.backgroundColor +
 
1217
                                                                                                                        data.backgroundRepeat +
 
1218
                                                                                                                        data.backgroundPosition +
 
1219
                                                                                                                        data.backgroundTransition +
 
1220
                                                                                                                        data.backgroundOpacity );
 
1221
                }
 
1222
 
 
1223
                // Additional and optional background properties
 
1224
                if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
 
1225
                if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
 
1226
                if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
 
1227
 
 
1228
                // Background image options are set on the content wrapper
 
1229
                if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
 
1230
                if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
 
1231
                if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
 
1232
                if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
 
1233
 
 
1234
                // If this slide has a background color, we add a class that
 
1235
                // signals if it is light or dark. If the slide has no background
 
1236
                // color, no class will be added
 
1237
                var contrastColor = data.backgroundColor;
 
1238
 
 
1239
                // If no bg color was found, check the computed background
 
1240
                if( !contrastColor ) {
 
1241
                        var computedBackgroundStyle = window.getComputedStyle( element );
 
1242
                        if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
 
1243
                                contrastColor = computedBackgroundStyle.backgroundColor;
 
1244
                        }
 
1245
                }
 
1246
 
 
1247
                if( contrastColor ) {
 
1248
                        var rgb = colorToRgb( contrastColor );
 
1249
 
 
1250
                        // Ignore fully transparent backgrounds. Some browsers return
 
1251
                        // rgba(0,0,0,0) when reading the computed background color of
 
1252
                        // an element with no background
 
1253
                        if( rgb && rgb.a !== 0 ) {
 
1254
                                if( colorBrightness( contrastColor ) < 128 ) {
 
1255
                                        slide.classList.add( 'has-dark-background' );
 
1256
                                }
 
1257
                                else {
 
1258
                                        slide.classList.add( 'has-light-background' );
 
1259
                                }
 
1260
                        }
 
1261
                }
 
1262
 
 
1263
        }
 
1264
 
 
1265
        /**
 
1266
         * Registers a listener to postMessage events, this makes it
 
1267
         * possible to call all reveal.js API methods from another
 
1268
         * window. For example:
 
1269
         *
 
1270
         * revealWindow.postMessage( JSON.stringify({
 
1271
         *   method: 'slide',
 
1272
         *   args: [ 2 ]
 
1273
         * }), '*' );
 
1274
         */
 
1275
        function setupPostMessage() {
 
1276
 
 
1277
                if( config.postMessage ) {
 
1278
                        window.addEventListener( 'message', function ( event ) {
 
1279
                                var data = event.data;
 
1280
 
 
1281
                                // Make sure we're dealing with JSON
 
1282
                                if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
 
1283
                                        data = JSON.parse( data );
 
1284
 
 
1285
                                        // Check if the requested method can be found
 
1286
                                        if( data.method && typeof Reveal[data.method] === 'function' ) {
 
1287
                                                Reveal[data.method].apply( Reveal, data.args );
 
1288
                                        }
 
1289
                                }
 
1290
                        }, false );
 
1291
                }
 
1292
 
 
1293
        }
 
1294
 
 
1295
        /**
 
1296
         * Applies the configuration settings from the config
 
1297
         * object. May be called multiple times.
 
1298
         *
 
1299
         * @param {object} options
 
1300
         */
 
1301
        function configure( options ) {
 
1302
 
 
1303
                var oldTransition = config.transition;
 
1304
 
 
1305
                // New config options may be passed when this method
 
1306
                // is invoked through the API after initialization
 
1307
                if( typeof options === 'object' ) extend( config, options );
 
1308
 
 
1309
                // Abort if reveal.js hasn't finished loading, config
 
1310
                // changes will be applied automatically once loading
 
1311
                // finishes
 
1312
                if( loaded === false ) return;
 
1313
 
 
1314
                var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
 
1315
 
 
1316
                // Remove the previously configured transition class
 
1317
                dom.wrapper.classList.remove( oldTransition );
 
1318
 
 
1319
                // Force linear transition based on browser capabilities
 
1320
                if( features.transforms3d === false ) config.transition = 'linear';
 
1321
 
 
1322
                dom.wrapper.classList.add( config.transition );
 
1323
 
 
1324
                dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
 
1325
                dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
 
1326
 
 
1327
                dom.controls.style.display = config.controls ? 'block' : 'none';
 
1328
                dom.progress.style.display = config.progress ? 'block' : 'none';
 
1329
 
 
1330
                dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout );
 
1331
                dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
 
1332
 
 
1333
                if( config.shuffle ) {
 
1334
                        shuffle();
 
1335
                }
 
1336
 
 
1337
                if( config.rtl ) {
 
1338
                        dom.wrapper.classList.add( 'rtl' );
 
1339
                }
 
1340
                else {
 
1341
                        dom.wrapper.classList.remove( 'rtl' );
 
1342
                }
 
1343
 
 
1344
                if( config.center ) {
 
1345
                        dom.wrapper.classList.add( 'center' );
 
1346
                }
 
1347
                else {
 
1348
                        dom.wrapper.classList.remove( 'center' );
 
1349
                }
 
1350
 
 
1351
                // Exit the paused mode if it was configured off
 
1352
                if( config.pause === false ) {
 
1353
                        resume();
 
1354
                }
 
1355
 
 
1356
                if( config.showNotes ) {
 
1357
                        dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
 
1358
                }
 
1359
 
 
1360
                if( config.mouseWheel ) {
 
1361
                        document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
 
1362
                        document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
 
1363
                }
 
1364
                else {
 
1365
                        document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
 
1366
                        document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
 
1367
                }
 
1368
 
 
1369
                // Rolling 3D links
 
1370
                if( config.rollingLinks ) {
 
1371
                        enableRollingLinks();
 
1372
                }
 
1373
                else {
 
1374
                        disableRollingLinks();
 
1375
                }
 
1376
 
 
1377
                // Auto-hide the mouse pointer when its inactive
 
1378
                if( config.hideInactiveCursor ) {
 
1379
                        document.addEventListener( 'mousemove', onDocumentCursorActive, false );
 
1380
                        document.addEventListener( 'mousedown', onDocumentCursorActive, false );
 
1381
                }
 
1382
                else {
 
1383
                        showCursor();
 
1384
 
 
1385
                        document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
 
1386
                        document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
 
1387
                }
 
1388
 
 
1389
                // Iframe link previews
 
1390
                if( config.previewLinks ) {
 
1391
                        enablePreviewLinks();
 
1392
                        disablePreviewLinks( '[data-preview-link=false]' );
 
1393
                }
 
1394
                else {
 
1395
                        disablePreviewLinks();
 
1396
                        enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
 
1397
                }
 
1398
 
 
1399
                // Remove existing auto-slide controls
 
1400
                if( autoSlidePlayer ) {
 
1401
                        autoSlidePlayer.destroy();
 
1402
                        autoSlidePlayer = null;
 
1403
                }
 
1404
 
 
1405
                // Generate auto-slide controls if needed
 
1406
                if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
 
1407
                        autoSlidePlayer = new Playback( dom.wrapper, function() {
 
1408
                                return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
 
1409
                        } );
 
1410
 
 
1411
                        autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
 
1412
                        autoSlidePaused = false;
 
1413
                }
 
1414
 
 
1415
                // When fragments are turned off they should be visible
 
1416
                if( config.fragments === false ) {
 
1417
                        toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
 
1418
                                element.classList.add( 'visible' );
 
1419
                                element.classList.remove( 'current-fragment' );
 
1420
                        } );
 
1421
                }
 
1422
 
 
1423
                // Slide numbers
 
1424
                var slideNumberDisplay = 'none';
 
1425
                if( config.slideNumber && !isPrintingPDF() ) {
 
1426
                        if( config.showSlideNumber === 'all' ) {
 
1427
                                slideNumberDisplay = 'block';
 
1428
                        }
 
1429
                        else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
 
1430
                                slideNumberDisplay = 'block';
 
1431
                        }
 
1432
                }
 
1433
 
 
1434
                dom.slideNumber.style.display = slideNumberDisplay;
 
1435
 
 
1436
                // Add the navigation mode to the DOM so we can adjust styling
 
1437
                if( config.navigationMode !== 'default' ) {
 
1438
                        dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
 
1439
                }
 
1440
                else {
 
1441
                        dom.wrapper.removeAttribute( 'data-navigation-mode' );
 
1442
                }
 
1443
 
 
1444
                // Define our contextual list of keyboard shortcuts
 
1445
                if( config.navigationMode === 'linear' ) {
 
1446
                        keyboardShortcuts['&#8594;  ,  &#8595;  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide';
 
1447
                        keyboardShortcuts['&#8592;  ,  &#8593;  ,  P  ,  H  ,  K']           = 'Previous slide';
 
1448
                }
 
1449
                else {
 
1450
                        keyboardShortcuts['N  ,  SPACE']   = 'Next slide';
 
1451
                        keyboardShortcuts['P']             = 'Previous slide';
 
1452
                        keyboardShortcuts['&#8592;  ,  H'] = 'Navigate left';
 
1453
                        keyboardShortcuts['&#8594;  ,  L'] = 'Navigate right';
 
1454
                        keyboardShortcuts['&#8593;  ,  K'] = 'Navigate up';
 
1455
                        keyboardShortcuts['&#8595;  ,  J'] = 'Navigate down';
 
1456
                }
 
1457
 
 
1458
                keyboardShortcuts['Home  ,  &#8984;/CTRL &#8592;'] = 'First slide';
 
1459
                keyboardShortcuts['End  ,  &#8984;/CTRL &#8594;']  = 'Last slide';
 
1460
                keyboardShortcuts['B  ,  .']                       = 'Pause';
 
1461
                keyboardShortcuts['F']                             = 'Fullscreen';
 
1462
                keyboardShortcuts['ESC, O']                        = 'Slide overview';
 
1463
 
 
1464
                sync();
 
1465
 
 
1466
        }
 
1467
 
 
1468
        /**
 
1469
         * Binds all event listeners.
 
1470
         */
 
1471
        function addEventListeners() {
 
1472
 
 
1473
                eventsAreBound = true;
 
1474
 
 
1475
                window.addEventListener( 'hashchange', onWindowHashChange, false );
 
1476
                window.addEventListener( 'resize', onWindowResize, false );
 
1477
 
 
1478
                if( config.touch ) {
 
1479
                        if( 'onpointerdown' in window ) {
 
1480
                                // Use W3C pointer events
 
1481
                                dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
 
1482
                                dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
 
1483
                                dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
 
1484
                        }
 
1485
                        else if( window.navigator.msPointerEnabled ) {
 
1486
                                // IE 10 uses prefixed version of pointer events
 
1487
                                dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
 
1488
                                dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
 
1489
                                dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
 
1490
                        }
 
1491
                        else {
 
1492
                                // Fall back to touch events
 
1493
                                dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
 
1494
                                dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
 
1495
                                dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
 
1496
                        }
 
1497
                }
 
1498
 
 
1499
                if( config.keyboard ) {
 
1500
                        document.addEventListener( 'keydown', onDocumentKeyDown, false );
 
1501
                        document.addEventListener( 'keypress', onDocumentKeyPress, false );
 
1502
                }
 
1503
 
 
1504
                if( config.progress && dom.progress ) {
 
1505
                        dom.progress.addEventListener( 'click', onProgressClicked, false );
 
1506
                }
 
1507
 
 
1508
                dom.pauseOverlay.addEventListener( 'click', resume, false );
 
1509
 
 
1510
                if( config.focusBodyOnPageVisibilityChange ) {
 
1511
                        var visibilityChange;
 
1512
 
 
1513
                        if( 'hidden' in document ) {
 
1514
                                visibilityChange = 'visibilitychange';
 
1515
                        }
 
1516
                        else if( 'msHidden' in document ) {
 
1517
                                visibilityChange = 'msvisibilitychange';
 
1518
                        }
 
1519
                        else if( 'webkitHidden' in document ) {
 
1520
                                visibilityChange = 'webkitvisibilitychange';
 
1521
                        }
 
1522
 
 
1523
                        if( visibilityChange ) {
 
1524
                                document.addEventListener( visibilityChange, onPageVisibilityChange, false );
 
1525
                        }
 
1526
                }
 
1527
 
 
1528
                // Listen to both touch and click events, in case the device
 
1529
                // supports both
 
1530
                var pointerEvents = [ 'touchstart', 'click' ];
 
1531
 
 
1532
                // Only support touch for Android, fixes double navigations in
 
1533
                // stock browser
 
1534
                if( UA.match( /android/gi ) ) {
 
1535
                        pointerEvents = [ 'touchstart' ];
 
1536
                }
 
1537
 
 
1538
                pointerEvents.forEach( function( eventName ) {
 
1539
                        dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
 
1540
                        dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
 
1541
                        dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
 
1542
                        dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
 
1543
                        dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
 
1544
                        dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
 
1545
                } );
 
1546
 
 
1547
        }
 
1548
 
 
1549
        /**
 
1550
         * Unbinds all event listeners.
 
1551
         */
 
1552
        function removeEventListeners() {
 
1553
 
 
1554
                eventsAreBound = false;
 
1555
 
 
1556
                document.removeEventListener( 'keydown', onDocumentKeyDown, false );
 
1557
                document.removeEventListener( 'keypress', onDocumentKeyPress, false );
 
1558
                window.removeEventListener( 'hashchange', onWindowHashChange, false );
 
1559
                window.removeEventListener( 'resize', onWindowResize, false );
 
1560
 
 
1561
                dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
 
1562
                dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
 
1563
                dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
 
1564
 
 
1565
                dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
 
1566
                dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
 
1567
                dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
 
1568
 
 
1569
                dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
 
1570
                dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
 
1571
                dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
 
1572
 
 
1573
                dom.pauseOverlay.removeEventListener( 'click', resume, false );
 
1574
 
 
1575
                if ( config.progress && dom.progress ) {
 
1576
                        dom.progress.removeEventListener( 'click', onProgressClicked, false );
 
1577
                }
 
1578
 
 
1579
                [ 'touchstart', 'click' ].forEach( function( eventName ) {
 
1580
                        dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
 
1581
                        dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
 
1582
                        dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
 
1583
                        dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
 
1584
                        dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
 
1585
                        dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
 
1586
                } );
 
1587
 
 
1588
        }
 
1589
 
 
1590
        /**
 
1591
         * Registers a new plugin with this reveal.js instance.
 
1592
         *
 
1593
         * reveal.js waits for all regisered plugins to initialize
 
1594
         * before considering itself ready, as long as the plugin
 
1595
         * is registered before calling `Reveal.initialize()`.
 
1596
         */
 
1597
        function registerPlugin( id, plugin ) {
 
1598
 
 
1599
                if( plugins[id] === undefined ) {
 
1600
                        plugins[id] = plugin;
 
1601
 
 
1602
                        // If a plugin is registered after reveal.js is loaded,
 
1603
                        // initialize it right away
 
1604
                        if( loaded && typeof plugin.init === 'function' ) {
 
1605
                                plugin.init();
 
1606
                        }
 
1607
                }
 
1608
                else {
 
1609
                        console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
 
1610
                }
 
1611
 
 
1612
        }
 
1613
 
 
1614
        /**
 
1615
         * Checks if a specific plugin has been registered.
 
1616
         *
 
1617
         * @param {String} id Unique plugin identifier
 
1618
         */
 
1619
        function hasPlugin( id ) {
 
1620
 
 
1621
                return !!plugins[id];
 
1622
 
 
1623
        }
 
1624
 
 
1625
        /**
 
1626
         * Returns the specific plugin instance, if a plugin
 
1627
         * with the given ID has been registered.
 
1628
         *
 
1629
         * @param {String} id Unique plugin identifier
 
1630
         */
 
1631
        function getPlugin( id ) {
 
1632
 
 
1633
                return plugins[id];
 
1634
 
 
1635
        }
 
1636
 
 
1637
        /**
 
1638
         * Add a custom key binding with optional description to
 
1639
         * be added to the help screen.
 
1640
         */
 
1641
        function addKeyBinding( binding, callback ) {
 
1642
 
 
1643
                if( typeof binding === 'object' && binding.keyCode ) {
 
1644
                        registeredKeyBindings[binding.keyCode] = {
 
1645
                                callback: callback,
 
1646
                                key: binding.key,
 
1647
                                description: binding.description
 
1648
                        };
 
1649
                }
 
1650
                else {
 
1651
                        registeredKeyBindings[binding] = {
 
1652
                                callback: callback,
 
1653
                                key: null,
 
1654
                                description: null
 
1655
                        };
 
1656
                }
 
1657
 
 
1658
        }
 
1659
 
 
1660
        /**
 
1661
         * Removes the specified custom key binding.
 
1662
         */
 
1663
        function removeKeyBinding( keyCode ) {
 
1664
 
 
1665
                delete registeredKeyBindings[keyCode];
 
1666
 
 
1667
        }
 
1668
 
 
1669
        /**
 
1670
         * Extend object a with the properties of object b.
 
1671
         * If there's a conflict, object b takes precedence.
 
1672
         *
 
1673
         * @param {object} a
 
1674
         * @param {object} b
 
1675
         */
 
1676
        function extend( a, b ) {
 
1677
 
 
1678
                for( var i in b ) {
 
1679
                        a[ i ] = b[ i ];
 
1680
                }
 
1681
 
 
1682
                return a;
 
1683
 
 
1684
        }
 
1685
 
 
1686
        /**
 
1687
         * Converts the target object to an array.
 
1688
         *
 
1689
         * @param {object} o
 
1690
         * @return {object[]}
 
1691
         */
 
1692
        function toArray( o ) {
 
1693
 
 
1694
                return Array.prototype.slice.call( o );
 
1695
 
 
1696
        }
 
1697
 
 
1698
        /**
 
1699
         * Utility for deserializing a value.
 
1700
         *
 
1701
         * @param {*} value
 
1702
         * @return {*}
 
1703
         */
 
1704
        function deserialize( value ) {
 
1705
 
 
1706
                if( typeof value === 'string' ) {
 
1707
                        if( value === 'null' ) return null;
 
1708
                        else if( value === 'true' ) return true;
 
1709
                        else if( value === 'false' ) return false;
 
1710
                        else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
 
1711
                }
 
1712
 
 
1713
                return value;
 
1714
 
 
1715
        }
 
1716
 
 
1717
        /**
 
1718
         * Measures the distance in pixels between point a
 
1719
         * and point b.
 
1720
         *
 
1721
         * @param {object} a point with x/y properties
 
1722
         * @param {object} b point with x/y properties
 
1723
         *
 
1724
         * @return {number}
 
1725
         */
 
1726
        function distanceBetween( a, b ) {
 
1727
 
 
1728
                var dx = a.x - b.x,
 
1729
                        dy = a.y - b.y;
 
1730
 
 
1731
                return Math.sqrt( dx*dx + dy*dy );
 
1732
 
 
1733
        }
 
1734
 
 
1735
        /**
 
1736
         * Applies a CSS transform to the target element.
 
1737
         *
 
1738
         * @param {HTMLElement} element
 
1739
         * @param {string} transform
 
1740
         */
 
1741
        function transformElement( element, transform ) {
 
1742
 
 
1743
                element.style.WebkitTransform = transform;
 
1744
                element.style.MozTransform = transform;
 
1745
                element.style.msTransform = transform;
 
1746
                element.style.transform = transform;
 
1747
 
 
1748
        }
 
1749
 
 
1750
        /**
 
1751
         * Applies CSS transforms to the slides container. The container
 
1752
         * is transformed from two separate sources: layout and the overview
 
1753
         * mode.
 
1754
         *
 
1755
         * @param {object} transforms
 
1756
         */
 
1757
        function transformSlides( transforms ) {
 
1758
 
 
1759
                // Pick up new transforms from arguments
 
1760
                if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
 
1761
                if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
 
1762
 
 
1763
                // Apply the transforms to the slides container
 
1764
                if( slidesTransform.layout ) {
 
1765
                        transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
 
1766
                }
 
1767
                else {
 
1768
                        transformElement( dom.slides, slidesTransform.overview );
 
1769
                }
 
1770
 
 
1771
        }
 
1772
 
 
1773
        /**
 
1774
         * Injects the given CSS styles into the DOM.
 
1775
         *
 
1776
         * @param {string} value
 
1777
         */
 
1778
        function injectStyleSheet( value ) {
 
1779
 
 
1780
                var tag = document.createElement( 'style' );
 
1781
                tag.type = 'text/css';
 
1782
                if( tag.styleSheet ) {
 
1783
                        tag.styleSheet.cssText = value;
 
1784
                }
 
1785
                else {
 
1786
                        tag.appendChild( document.createTextNode( value ) );
 
1787
                }
 
1788
                document.getElementsByTagName( 'head' )[0].appendChild( tag );
 
1789
 
 
1790
        }
 
1791
 
 
1792
        /**
 
1793
         * Find the closest parent that matches the given
 
1794
         * selector.
 
1795
         *
 
1796
         * @param {HTMLElement} target The child element
 
1797
         * @param {String} selector The CSS selector to match
 
1798
         * the parents against
 
1799
         *
 
1800
         * @return {HTMLElement} The matched parent or null
 
1801
         * if no matching parent was found
 
1802
         */
 
1803
        function closestParent( target, selector ) {
 
1804
 
 
1805
                var parent = target.parentNode;
 
1806
 
 
1807
                while( parent ) {
 
1808
 
 
1809
                        // There's some overhead doing this each time, we don't
 
1810
                        // want to rewrite the element prototype but should still
 
1811
                        // be enough to feature detect once at startup...
 
1812
                        var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
 
1813
 
 
1814
                        // If we find a match, we're all set
 
1815
                        if( matchesMethod && matchesMethod.call( parent, selector ) ) {
 
1816
                                return parent;
 
1817
                        }
 
1818
 
 
1819
                        // Keep searching
 
1820
                        parent = parent.parentNode;
 
1821
 
 
1822
                }
 
1823
 
 
1824
                return null;
 
1825
 
 
1826
        }
 
1827
 
 
1828
        /**
 
1829
         * Converts various color input formats to an {r:0,g:0,b:0} object.
 
1830
         *
 
1831
         * @param {string} color The string representation of a color
 
1832
         * @example
 
1833
         * colorToRgb('#000');
 
1834
         * @example
 
1835
         * colorToRgb('#000000');
 
1836
         * @example
 
1837
         * colorToRgb('rgb(0,0,0)');
 
1838
         * @example
 
1839
         * colorToRgb('rgba(0,0,0)');
 
1840
         *
 
1841
         * @return {{r: number, g: number, b: number, [a]: number}|null}
 
1842
         */
 
1843
        function colorToRgb( color ) {
 
1844
 
 
1845
                var hex3 = color.match( /^#([0-9a-f]{3})$/i );
 
1846
                if( hex3 && hex3[1] ) {
 
1847
                        hex3 = hex3[1];
 
1848
                        return {
 
1849
                                r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
 
1850
                                g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
 
1851
                                b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
 
1852
                        };
 
1853
                }
 
1854
 
 
1855
                var hex6 = color.match( /^#([0-9a-f]{6})$/i );
 
1856
                if( hex6 && hex6[1] ) {
 
1857
                        hex6 = hex6[1];
 
1858
                        return {
 
1859
                                r: parseInt( hex6.substr( 0, 2 ), 16 ),
 
1860
                                g: parseInt( hex6.substr( 2, 2 ), 16 ),
 
1861
                                b: parseInt( hex6.substr( 4, 2 ), 16 )
 
1862
                        };
 
1863
                }
 
1864
 
 
1865
                var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
 
1866
                if( rgb ) {
 
1867
                        return {
 
1868
                                r: parseInt( rgb[1], 10 ),
 
1869
                                g: parseInt( rgb[2], 10 ),
 
1870
                                b: parseInt( rgb[3], 10 )
 
1871
                        };
 
1872
                }
 
1873
 
 
1874
                var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
 
1875
                if( rgba ) {
 
1876
                        return {
 
1877
                                r: parseInt( rgba[1], 10 ),
 
1878
                                g: parseInt( rgba[2], 10 ),
 
1879
                                b: parseInt( rgba[3], 10 ),
 
1880
                                a: parseFloat( rgba[4] )
 
1881
                        };
 
1882
                }
 
1883
 
 
1884
                return null;
 
1885
 
 
1886
        }
 
1887
 
 
1888
        /**
 
1889
         * Calculates brightness on a scale of 0-255.
 
1890
         *
 
1891
         * @param {string} color See colorToRgb for supported formats.
 
1892
         * @see {@link colorToRgb}
 
1893
         */
 
1894
        function colorBrightness( color ) {
 
1895
 
 
1896
                if( typeof color === 'string' ) color = colorToRgb( color );
 
1897
 
 
1898
                if( color ) {
 
1899
                        return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
 
1900
                }
 
1901
 
 
1902
                return null;
 
1903
 
 
1904
        }
 
1905
 
 
1906
        /**
 
1907
         * Returns the remaining height within the parent of the
 
1908
         * target element.
 
1909
         *
 
1910
         * remaining height = [ configured parent height ] - [ current parent height ]
 
1911
         *
 
1912
         * @param {HTMLElement} element
 
1913
         * @param {number} [height]
 
1914
         */
 
1915
        function getRemainingHeight( element, height ) {
 
1916
 
 
1917
                height = height || 0;
 
1918
 
 
1919
                if( element ) {
 
1920
                        var newHeight, oldHeight = element.style.height;
 
1921
 
 
1922
                        // Change the .stretch element height to 0 in order find the height of all
 
1923
                        // the other elements
 
1924
                        element.style.height = '0px';
 
1925
 
 
1926
                        // In Overview mode, the parent (.slide) height is set of 700px.
 
1927
                        // Restore it temporarily to its natural height.
 
1928
                        element.parentNode.style.height = 'auto';
 
1929
 
 
1930
                        newHeight = height - element.parentNode.offsetHeight;
 
1931
 
 
1932
                        // Restore the old height, just in case
 
1933
                        element.style.height = oldHeight + 'px';
 
1934
 
 
1935
                        // Clear the parent (.slide) height. .removeProperty works in IE9+
 
1936
                        element.parentNode.style.removeProperty('height');
 
1937
 
 
1938
                        return newHeight;
 
1939
                }
 
1940
 
 
1941
                return height;
 
1942
 
 
1943
        }
 
1944
 
 
1945
        /**
 
1946
         * Checks if this instance is being used to print a PDF.
 
1947
         */
 
1948
        function isPrintingPDF() {
 
1949
 
 
1950
                return ( /print-pdf/gi ).test( window.location.search );
 
1951
 
 
1952
        }
 
1953
 
 
1954
        /**
 
1955
         * Hides the address bar if we're on a mobile device.
 
1956
         */
 
1957
        function hideAddressBar() {
 
1958
 
 
1959
                if( config.hideAddressBar && isMobileDevice ) {
 
1960
                        // Events that should trigger the address bar to hide
 
1961
                        window.addEventListener( 'load', removeAddressBar, false );
 
1962
                        window.addEventListener( 'orientationchange', removeAddressBar, false );
 
1963
                }
 
1964
 
 
1965
        }
 
1966
 
 
1967
        /**
 
1968
         * Causes the address bar to hide on mobile devices,
 
1969
         * more vertical space ftw.
 
1970
         */
 
1971
        function removeAddressBar() {
 
1972
 
 
1973
                setTimeout( function() {
 
1974
                        window.scrollTo( 0, 1 );
 
1975
                }, 10 );
 
1976
 
 
1977
        }
 
1978
 
 
1979
        /**
 
1980
         * Dispatches an event of the specified type from the
 
1981
         * reveal DOM element.
 
1982
         */
 
1983
        function dispatchEvent( type, args ) {
 
1984
 
 
1985
                var event = document.createEvent( 'HTMLEvents', 1, 2 );
 
1986
                event.initEvent( type, true, true );
 
1987
                extend( event, args );
 
1988
                dom.wrapper.dispatchEvent( event );
 
1989
 
 
1990
                // If we're in an iframe, post each reveal.js event to the
 
1991
                // parent window. Used by the notes plugin
 
1992
                if( config.postMessageEvents && window.parent !== window.self ) {
 
1993
                        window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
 
1994
                }
 
1995
 
 
1996
        }
 
1997
 
 
1998
        /**
 
1999
         * Wrap all links in 3D goodness.
 
2000
         */
 
2001
        function enableRollingLinks() {
 
2002
 
 
2003
                if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
 
2004
                        var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
 
2005
 
 
2006
                        for( var i = 0, len = anchors.length; i < len; i++ ) {
 
2007
                                var anchor = anchors[i];
 
2008
 
 
2009
                                if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
 
2010
                                        var span = document.createElement('span');
 
2011
                                        span.setAttribute('data-title', anchor.text);
 
2012
                                        span.innerHTML = anchor.innerHTML;
 
2013
 
 
2014
                                        anchor.classList.add( 'roll' );
 
2015
                                        anchor.innerHTML = '';
 
2016
                                        anchor.appendChild(span);
 
2017
                                }
 
2018
                        }
 
2019
                }
 
2020
 
 
2021
        }
 
2022
 
 
2023
        /**
 
2024
         * Unwrap all 3D links.
 
2025
         */
 
2026
        function disableRollingLinks() {
 
2027
 
 
2028
                var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
 
2029
 
 
2030
                for( var i = 0, len = anchors.length; i < len; i++ ) {
 
2031
                        var anchor = anchors[i];
 
2032
                        var span = anchor.querySelector( 'span' );
 
2033
 
 
2034
                        if( span ) {
 
2035
                                anchor.classList.remove( 'roll' );
 
2036
                                anchor.innerHTML = span.innerHTML;
 
2037
                        }
 
2038
                }
 
2039
 
 
2040
        }
 
2041
 
 
2042
        /**
 
2043
         * Bind preview frame links.
 
2044
         *
 
2045
         * @param {string} [selector=a] - selector for anchors
 
2046
         */
 
2047
        function enablePreviewLinks( selector ) {
 
2048
 
 
2049
                var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
 
2050
 
 
2051
                anchors.forEach( function( element ) {
 
2052
                        if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
 
2053
                                element.addEventListener( 'click', onPreviewLinkClicked, false );
 
2054
                        }
 
2055
                } );
 
2056
 
 
2057
        }
 
2058
 
 
2059
        /**
 
2060
         * Unbind preview frame links.
 
2061
         */
 
2062
        function disablePreviewLinks( selector ) {
 
2063
 
 
2064
                var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
 
2065
 
 
2066
                anchors.forEach( function( element ) {
 
2067
                        if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
 
2068
                                element.removeEventListener( 'click', onPreviewLinkClicked, false );
 
2069
                        }
 
2070
                } );
 
2071
 
 
2072
        }
 
2073
 
 
2074
        /**
 
2075
         * Opens a preview window for the target URL.
 
2076
         *
 
2077
         * @param {string} url - url for preview iframe src
 
2078
         */
 
2079
        function showPreview( url ) {
 
2080
 
 
2081
                closeOverlay();
 
2082
 
 
2083
                dom.overlay = document.createElement( 'div' );
 
2084
                dom.overlay.classList.add( 'overlay' );
 
2085
                dom.overlay.classList.add( 'overlay-preview' );
 
2086
                dom.wrapper.appendChild( dom.overlay );
 
2087
 
 
2088
                dom.overlay.innerHTML = [
 
2089
                        '<header>',
 
2090
                                '<a class="close" href="#"><span class="icon"></span></a>',
 
2091
                                '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
 
2092
                        '</header>',
 
2093
                        '<div class="spinner"></div>',
 
2094
                        '<div class="viewport">',
 
2095
                                '<iframe src="'+ url +'"></iframe>',
 
2096
                                '<small class="viewport-inner">',
 
2097
                                        '<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
 
2098
                                '</small>',
 
2099
                        '</div>'
 
2100
                ].join('');
 
2101
 
 
2102
                dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
 
2103
                        dom.overlay.classList.add( 'loaded' );
 
2104
                }, false );
 
2105
 
 
2106
                dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
 
2107
                        closeOverlay();
 
2108
                        event.preventDefault();
 
2109
                }, false );
 
2110
 
 
2111
                dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
 
2112
                        closeOverlay();
 
2113
                }, false );
 
2114
 
 
2115
                setTimeout( function() {
 
2116
                        dom.overlay.classList.add( 'visible' );
 
2117
                }, 1 );
 
2118
 
 
2119
        }
 
2120
 
 
2121
        /**
 
2122
         * Open or close help overlay window.
 
2123
         *
 
2124
         * @param {Boolean} [override] Flag which overrides the
 
2125
         * toggle logic and forcibly sets the desired state. True means
 
2126
         * help is open, false means it's closed.
 
2127
         */
 
2128
        function toggleHelp( override ){
 
2129
 
 
2130
                if( typeof override === 'boolean' ) {
 
2131
                        override ? showHelp() : closeOverlay();
 
2132
                }
 
2133
                else {
 
2134
                        if( dom.overlay ) {
 
2135
                                closeOverlay();
 
2136
                        }
 
2137
                        else {
 
2138
                                showHelp();
 
2139
                        }
 
2140
                }
 
2141
        }
 
2142
 
 
2143
        /**
 
2144
         * Opens an overlay window with help material.
 
2145
         */
 
2146
        function showHelp() {
 
2147
 
 
2148
                if( config.help ) {
 
2149
 
 
2150
                        closeOverlay();
 
2151
 
 
2152
                        dom.overlay = document.createElement( 'div' );
 
2153
                        dom.overlay.classList.add( 'overlay' );
 
2154
                        dom.overlay.classList.add( 'overlay-help' );
 
2155
                        dom.wrapper.appendChild( dom.overlay );
 
2156
 
 
2157
                        var html = '<p class="title">Keyboard Shortcuts</p><br/>';
 
2158
 
 
2159
                        html += '<table><th>KEY</th><th>ACTION</th>';
 
2160
                        for( var key in keyboardShortcuts ) {
 
2161
                                html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
 
2162
                        }
 
2163
 
 
2164
                        // Add custom key bindings that have associated descriptions
 
2165
                        for( var binding in registeredKeyBindings ) {
 
2166
                                if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
 
2167
                                        html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
 
2168
                                }
 
2169
                        }
 
2170
 
 
2171
                        html += '</table>';
 
2172
 
 
2173
                        dom.overlay.innerHTML = [
 
2174
                                '<header>',
 
2175
                                        '<a class="close" href="#"><span class="icon"></span></a>',
 
2176
                                '</header>',
 
2177
                                '<div class="viewport">',
 
2178
                                        '<div class="viewport-inner">'+ html +'</div>',
 
2179
                                '</div>'
 
2180
                        ].join('');
 
2181
 
 
2182
                        dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
 
2183
                                closeOverlay();
 
2184
                                event.preventDefault();
 
2185
                        }, false );
 
2186
 
 
2187
                        setTimeout( function() {
 
2188
                                dom.overlay.classList.add( 'visible' );
 
2189
                        }, 1 );
 
2190
 
 
2191
                }
 
2192
 
 
2193
        }
 
2194
 
 
2195
        /**
 
2196
         * Closes any currently open overlay.
 
2197
         */
 
2198
        function closeOverlay() {
 
2199
 
 
2200
                if( dom.overlay ) {
 
2201
                        dom.overlay.parentNode.removeChild( dom.overlay );
 
2202
                        dom.overlay = null;
 
2203
                }
 
2204
 
 
2205
        }
 
2206
 
 
2207
        /**
 
2208
         * Applies JavaScript-controlled layout rules to the
 
2209
         * presentation.
 
2210
         */
 
2211
        function layout() {
 
2212
 
 
2213
                if( dom.wrapper && !isPrintingPDF() ) {
 
2214
 
 
2215
                        if( !config.disableLayout ) {
 
2216
 
 
2217
                                // On some mobile devices '100vh' is taller than the visible
 
2218
                                // viewport which leads to part of the presentation being
 
2219
                                // cut off. To work around this we define our own '--vh' custom
 
2220
                                // property where 100x adds up to the correct height.
 
2221
                                //
 
2222
                                // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
 
2223
                                if( isMobileDevice ) {
 
2224
                                        document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
 
2225
                                }
 
2226
 
 
2227
                                var size = getComputedSlideSize();
 
2228
 
 
2229
                                var oldScale = scale;
 
2230
 
 
2231
                                // Layout the contents of the slides
 
2232
                                layoutSlideContents( config.width, config.height );
 
2233
 
 
2234
                                dom.slides.style.width = size.width + 'px';
 
2235
                                dom.slides.style.height = size.height + 'px';
 
2236
 
 
2237
                                // Determine scale of content to fit within available space
 
2238
                                scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
 
2239
 
 
2240
                                // Respect max/min scale settings
 
2241
                                scale = Math.max( scale, config.minScale );
 
2242
                                scale = Math.min( scale, config.maxScale );
 
2243
 
 
2244
                                // Don't apply any scaling styles if scale is 1
 
2245
                                if( scale === 1 ) {
 
2246
                                        dom.slides.style.zoom = '';
 
2247
                                        dom.slides.style.left = '';
 
2248
                                        dom.slides.style.top = '';
 
2249
                                        dom.slides.style.bottom = '';
 
2250
                                        dom.slides.style.right = '';
 
2251
                                        transformSlides( { layout: '' } );
 
2252
                                }
 
2253
                                else {
 
2254
                                        // Prefer zoom for scaling up so that content remains crisp.
 
2255
                                        // Don't use zoom to scale down since that can lead to shifts
 
2256
                                        // in text layout/line breaks.
 
2257
                                        if( scale > 1 && features.zoom ) {
 
2258
                                                dom.slides.style.zoom = scale;
 
2259
                                                dom.slides.style.left = '';
 
2260
                                                dom.slides.style.top = '';
 
2261
                                                dom.slides.style.bottom = '';
 
2262
                                                dom.slides.style.right = '';
 
2263
                                                transformSlides( { layout: '' } );
 
2264
                                        }
 
2265
                                        // Apply scale transform as a fallback
 
2266
                                        else {
 
2267
                                                dom.slides.style.zoom = '';
 
2268
                                                dom.slides.style.left = '50%';
 
2269
                                                dom.slides.style.top = '50%';
 
2270
                                                dom.slides.style.bottom = 'auto';
 
2271
                                                dom.slides.style.right = 'auto';
 
2272
                                                transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
 
2273
                                        }
 
2274
                                }
 
2275
 
 
2276
                                // Select all slides, vertical and horizontal
 
2277
                                var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
 
2278
 
 
2279
                                for( var i = 0, len = slides.length; i < len; i++ ) {
 
2280
                                        var slide = slides[ i ];
 
2281
 
 
2282
                                        // Don't bother updating invisible slides
 
2283
                                        if( slide.style.display === 'none' ) {
 
2284
                                                continue;
 
2285
                                        }
 
2286
 
 
2287
                                        if( config.center || slide.classList.contains( 'center' ) ) {
 
2288
                                                // Vertical stacks are not centred since their section
 
2289
                                                // children will be
 
2290
                                                if( slide.classList.contains( 'stack' ) ) {
 
2291
                                                        slide.style.top = 0;
 
2292
                                                }
 
2293
                                                else {
 
2294
                                                        slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
 
2295
                                                }
 
2296
                                        }
 
2297
                                        else {
 
2298
                                                slide.style.top = '';
 
2299
                                        }
 
2300
 
 
2301
                                }
 
2302
 
 
2303
                                if( oldScale !== scale ) {
 
2304
                                        dispatchEvent( 'resize', {
 
2305
                                                'oldScale': oldScale,
 
2306
                                                'scale': scale,
 
2307
                                                'size': size
 
2308
                                        } );
 
2309
                                }
 
2310
                        }
 
2311
 
 
2312
                        updateProgress();
 
2313
                        updateParallax();
 
2314
 
 
2315
                        if( isOverview() ) {
 
2316
                                updateOverview();
 
2317
                        }
 
2318
 
 
2319
                }
 
2320
 
 
2321
        }
 
2322
 
 
2323
        /**
 
2324
         * Applies layout logic to the contents of all slides in
 
2325
         * the presentation.
 
2326
         *
 
2327
         * @param {string|number} width
 
2328
         * @param {string|number} height
 
2329
         */
 
2330
        function layoutSlideContents( width, height ) {
 
2331
 
 
2332
                // Handle sizing of elements with the 'stretch' class
 
2333
                toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
 
2334
 
 
2335
                        // Determine how much vertical space we can use
 
2336
                        var remainingHeight = getRemainingHeight( element, height );
 
2337
 
 
2338
                        // Consider the aspect ratio of media elements
 
2339
                        if( /(img|video)/gi.test( element.nodeName ) ) {
 
2340
                                var nw = element.naturalWidth || element.videoWidth,
 
2341
                                        nh = element.naturalHeight || element.videoHeight;
 
2342
 
 
2343
                                var es = Math.min( width / nw, remainingHeight / nh );
 
2344
 
 
2345
                                element.style.width = ( nw * es ) + 'px';
 
2346
                                element.style.height = ( nh * es ) + 'px';
 
2347
 
 
2348
                        }
 
2349
                        else {
 
2350
                                element.style.width = width + 'px';
 
2351
                                element.style.height = remainingHeight + 'px';
 
2352
                        }
 
2353
 
 
2354
                } );
 
2355
 
 
2356
        }
 
2357
 
 
2358
        /**
 
2359
         * Calculates the computed pixel size of our slides. These
 
2360
         * values are based on the width and height configuration
 
2361
         * options.
 
2362
         *
 
2363
         * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
 
2364
         * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
 
2365
         */
 
2366
        function getComputedSlideSize( presentationWidth, presentationHeight ) {
 
2367
 
 
2368
                var size = {
 
2369
                        // Slide size
 
2370
                        width: config.width,
 
2371
                        height: config.height,
 
2372
 
 
2373
                        // Presentation size
 
2374
                        presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
 
2375
                        presentationHeight: presentationHeight || dom.wrapper.offsetHeight
 
2376
                };
 
2377
 
 
2378
                // Reduce available space by margin
 
2379
                size.presentationWidth -= ( size.presentationWidth * config.margin );
 
2380
                size.presentationHeight -= ( size.presentationHeight * config.margin );
 
2381
 
 
2382
                // Slide width may be a percentage of available width
 
2383
                if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
 
2384
                        size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
 
2385
                }
 
2386
 
 
2387
                // Slide height may be a percentage of available height
 
2388
                if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
 
2389
                        size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
 
2390
                }
 
2391
 
 
2392
                return size;
 
2393
 
 
2394
        }
 
2395
 
 
2396
        /**
 
2397
         * Stores the vertical index of a stack so that the same
 
2398
         * vertical slide can be selected when navigating to and
 
2399
         * from the stack.
 
2400
         *
 
2401
         * @param {HTMLElement} stack The vertical stack element
 
2402
         * @param {string|number} [v=0] Index to memorize
 
2403
         */
 
2404
        function setPreviousVerticalIndex( stack, v ) {
 
2405
 
 
2406
                if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
 
2407
                        stack.setAttribute( 'data-previous-indexv', v || 0 );
 
2408
                }
 
2409
 
 
2410
        }
 
2411
 
 
2412
        /**
 
2413
         * Retrieves the vertical index which was stored using
 
2414
         * #setPreviousVerticalIndex() or 0 if no previous index
 
2415
         * exists.
 
2416
         *
 
2417
         * @param {HTMLElement} stack The vertical stack element
 
2418
         */
 
2419
        function getPreviousVerticalIndex( stack ) {
 
2420
 
 
2421
                if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
 
2422
                        // Prefer manually defined start-indexv
 
2423
                        var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
 
2424
 
 
2425
                        return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
 
2426
                }
 
2427
 
 
2428
                return 0;
 
2429
 
 
2430
        }
 
2431
 
 
2432
        /**
 
2433
         * Displays the overview of slides (quick nav) by scaling
 
2434
         * down and arranging all slide elements.
 
2435
         */
 
2436
        function activateOverview() {
 
2437
 
 
2438
                // Only proceed if enabled in config
 
2439
                if( config.overview && !isOverview() ) {
 
2440
 
 
2441
                        overview = true;
 
2442
 
 
2443
                        dom.wrapper.classList.add( 'overview' );
 
2444
                        dom.wrapper.classList.remove( 'overview-deactivating' );
 
2445
 
 
2446
                        if( features.overviewTransitions ) {
 
2447
                                setTimeout( function() {
 
2448
                                        dom.wrapper.classList.add( 'overview-animated' );
 
2449
                                }, 1 );
 
2450
                        }
 
2451
 
 
2452
                        // Don't auto-slide while in overview mode
 
2453
                        cancelAutoSlide();
 
2454
 
 
2455
                        // Move the backgrounds element into the slide container to
 
2456
                        // that the same scaling is applied
 
2457
                        dom.slides.appendChild( dom.background );
 
2458
 
 
2459
                        // Clicking on an overview slide navigates to it
 
2460
                        toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
 
2461
                                if( !slide.classList.contains( 'stack' ) ) {
 
2462
                                        slide.addEventListener( 'click', onOverviewSlideClicked, true );
 
2463
                                }
 
2464
                        } );
 
2465
 
 
2466
                        // Calculate slide sizes
 
2467
                        var margin = 70;
 
2468
                        var slideSize = getComputedSlideSize();
 
2469
                        overviewSlideWidth = slideSize.width + margin;
 
2470
                        overviewSlideHeight = slideSize.height + margin;
 
2471
 
 
2472
                        // Reverse in RTL mode
 
2473
                        if( config.rtl ) {
 
2474
                                overviewSlideWidth = -overviewSlideWidth;
 
2475
                        }
 
2476
 
 
2477
                        updateSlidesVisibility();
 
2478
                        layoutOverview();
 
2479
                        updateOverview();
 
2480
 
 
2481
                        layout();
 
2482
 
 
2483
                        // Notify observers of the overview showing
 
2484
                        dispatchEvent( 'overviewshown', {
 
2485
                                'indexh': indexh,
 
2486
                                'indexv': indexv,
 
2487
                                'currentSlide': currentSlide
 
2488
                        } );
 
2489
 
 
2490
                }
 
2491
 
 
2492
        }
 
2493
 
 
2494
        /**
 
2495
         * Uses CSS transforms to position all slides in a grid for
 
2496
         * display inside of the overview mode.
 
2497
         */
 
2498
        function layoutOverview() {
 
2499
 
 
2500
                // Layout slides
 
2501
                toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
 
2502
                        hslide.setAttribute( 'data-index-h', h );
 
2503
                        transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
 
2504
 
 
2505
                        if( hslide.classList.contains( 'stack' ) ) {
 
2506
 
 
2507
                                toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
 
2508
                                        vslide.setAttribute( 'data-index-h', h );
 
2509
                                        vslide.setAttribute( 'data-index-v', v );
 
2510
 
 
2511
                                        transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
 
2512
                                } );
 
2513
 
 
2514
                        }
 
2515
                } );
 
2516
 
 
2517
                // Layout slide backgrounds
 
2518
                toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
 
2519
                        transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
 
2520
 
 
2521
                        toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
 
2522
                                transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
 
2523
                        } );
 
2524
                } );
 
2525
 
 
2526
        }
 
2527
 
 
2528
        /**
 
2529
         * Moves the overview viewport to the current slides.
 
2530
         * Called each time the current slide changes.
 
2531
         */
 
2532
        function updateOverview() {
 
2533
 
 
2534
                var vmin = Math.min( window.innerWidth, window.innerHeight );
 
2535
                var scale = Math.max( vmin / 5, 150 ) / vmin;
 
2536
 
 
2537
                transformSlides( {
 
2538
                        overview: [
 
2539
                                'scale('+ scale +')',
 
2540
                                'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
 
2541
                                'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
 
2542
                        ].join( ' ' )
 
2543
                } );
 
2544
 
 
2545
        }
 
2546
 
 
2547
        /**
 
2548
         * Exits the slide overview and enters the currently
 
2549
         * active slide.
 
2550
         */
 
2551
        function deactivateOverview() {
 
2552
 
 
2553
                // Only proceed if enabled in config
 
2554
                if( config.overview ) {
 
2555
 
 
2556
                        overview = false;
 
2557
 
 
2558
                        dom.wrapper.classList.remove( 'overview' );
 
2559
                        dom.wrapper.classList.remove( 'overview-animated' );
 
2560
 
 
2561
                        // Temporarily add a class so that transitions can do different things
 
2562
                        // depending on whether they are exiting/entering overview, or just
 
2563
                        // moving from slide to slide
 
2564
                        dom.wrapper.classList.add( 'overview-deactivating' );
 
2565
 
 
2566
                        setTimeout( function () {
 
2567
                                dom.wrapper.classList.remove( 'overview-deactivating' );
 
2568
                        }, 1 );
 
2569
 
 
2570
                        // Move the background element back out
 
2571
                        dom.wrapper.appendChild( dom.background );
 
2572
 
 
2573
                        // Clean up changes made to slides
 
2574
                        toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
 
2575
                                transformElement( slide, '' );
 
2576
 
 
2577
                                slide.removeEventListener( 'click', onOverviewSlideClicked, true );
 
2578
                        } );
 
2579
 
 
2580
                        // Clean up changes made to backgrounds
 
2581
                        toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
 
2582
                                transformElement( background, '' );
 
2583
                        } );
 
2584
 
 
2585
                        transformSlides( { overview: '' } );
 
2586
 
 
2587
                        slide( indexh, indexv );
 
2588
 
 
2589
                        layout();
 
2590
 
 
2591
                        cueAutoSlide();
 
2592
 
 
2593
                        // Notify observers of the overview hiding
 
2594
                        dispatchEvent( 'overviewhidden', {
 
2595
                                'indexh': indexh,
 
2596
                                'indexv': indexv,
 
2597
                                'currentSlide': currentSlide
 
2598
                        } );
 
2599
 
 
2600
                }
 
2601
        }
 
2602
 
 
2603
        /**
 
2604
         * Toggles the slide overview mode on and off.
 
2605
         *
 
2606
         * @param {Boolean} [override] Flag which overrides the
 
2607
         * toggle logic and forcibly sets the desired state. True means
 
2608
         * overview is open, false means it's closed.
 
2609
         */
 
2610
        function toggleOverview( override ) {
 
2611
 
 
2612
                if( typeof override === 'boolean' ) {
 
2613
                        override ? activateOverview() : deactivateOverview();
 
2614
                }
 
2615
                else {
 
2616
                        isOverview() ? deactivateOverview() : activateOverview();
 
2617
                }
 
2618
 
 
2619
        }
 
2620
 
 
2621
        /**
 
2622
         * Checks if the overview is currently active.
 
2623
         *
 
2624
         * @return {Boolean} true if the overview is active,
 
2625
         * false otherwise
 
2626
         */
 
2627
        function isOverview() {
 
2628
 
 
2629
                return overview;
 
2630
 
 
2631
        }
 
2632
 
 
2633
        /**
 
2634
         * Return a hash URL that will resolve to the current slide location.
 
2635
         */
 
2636
        function locationHash() {
 
2637
 
 
2638
                var url = '/';
 
2639
 
 
2640
                // Attempt to create a named link based on the slide's ID
 
2641
                var id = currentSlide ? currentSlide.getAttribute( 'id' ) : null;
 
2642
                if( id ) {
 
2643
                        id = encodeURIComponent( id );
 
2644
                }
 
2645
 
 
2646
                var indexf;
 
2647
                if( config.fragmentInURL ) {
 
2648
                        indexf = getIndices().f;
 
2649
                }
 
2650
 
 
2651
                // If the current slide has an ID, use that as a named link,
 
2652
                // but we don't support named links with a fragment index
 
2653
                if( typeof id === 'string' && id.length && indexf === undefined ) {
 
2654
                        url = '/' + id;
 
2655
                }
 
2656
                // Otherwise use the /h/v index
 
2657
                else {
 
2658
                        var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
 
2659
                        if( indexh > 0 || indexv > 0 || indexf !== undefined ) url += indexh + hashIndexBase;
 
2660
                        if( indexv > 0 || indexf !== undefined ) url += '/' + (indexv + hashIndexBase );
 
2661
                        if( indexf !== undefined ) url += '/' + indexf;
 
2662
                }
 
2663
 
 
2664
                return url;
 
2665
 
 
2666
        }
 
2667
 
 
2668
        /**
 
2669
         * Checks if the current or specified slide is vertical
 
2670
         * (nested within another slide).
 
2671
         *
 
2672
         * @param {HTMLElement} [slide=currentSlide] The slide to check
 
2673
         * orientation of
 
2674
         * @return {Boolean}
 
2675
         */
 
2676
        function isVerticalSlide( slide ) {
 
2677
 
 
2678
                // Prefer slide argument, otherwise use current slide
 
2679
                slide = slide ? slide : currentSlide;
 
2680
 
 
2681
                return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
 
2682
 
 
2683
        }
 
2684
 
 
2685
        /**
 
2686
         * Handling the fullscreen functionality via the fullscreen API
 
2687
         *
 
2688
         * @see http://fullscreen.spec.whatwg.org/
 
2689
         * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
 
2690
         */
 
2691
        function enterFullscreen() {
 
2692
 
 
2693
                var element = document.documentElement;
 
2694
 
 
2695
                // Check which implementation is available
 
2696
                var requestMethod = element.requestFullscreen ||
 
2697
                                                        element.webkitRequestFullscreen ||
 
2698
                                                        element.webkitRequestFullScreen ||
 
2699
                                                        element.mozRequestFullScreen ||
 
2700
                                                        element.msRequestFullscreen;
 
2701
 
 
2702
                if( requestMethod ) {
 
2703
                        requestMethod.apply( element );
 
2704
                }
 
2705
 
 
2706
        }
 
2707
 
 
2708
        /**
 
2709
         * Shows the mouse pointer after it has been hidden with
 
2710
         * #hideCursor.
 
2711
         */
 
2712
        function showCursor() {
 
2713
 
 
2714
                if( cursorHidden ) {
 
2715
                        cursorHidden = false;
 
2716
                        dom.wrapper.style.cursor = '';
 
2717
                }
 
2718
 
 
2719
        }
 
2720
 
 
2721
        /**
 
2722
         * Hides the mouse pointer when it's on top of the .reveal
 
2723
         * container.
 
2724
         */
 
2725
        function hideCursor() {
 
2726
 
 
2727
                if( cursorHidden === false ) {
 
2728
                        cursorHidden = true;
 
2729
                        dom.wrapper.style.cursor = 'none';
 
2730
                }
 
2731
 
 
2732
        }
 
2733
 
 
2734
        /**
 
2735
         * Enters the paused mode which fades everything on screen to
 
2736
         * black.
 
2737
         */
 
2738
        function pause() {
 
2739
 
 
2740
                if( config.pause ) {
 
2741
                        var wasPaused = dom.wrapper.classList.contains( 'paused' );
 
2742
 
 
2743
                        cancelAutoSlide();
 
2744
                        dom.wrapper.classList.add( 'paused' );
 
2745
 
 
2746
                        if( wasPaused === false ) {
 
2747
                                dispatchEvent( 'paused' );
 
2748
                        }
 
2749
                }
 
2750
 
 
2751
        }
 
2752
 
 
2753
        /**
 
2754
         * Exits from the paused mode.
 
2755
         */
 
2756
        function resume() {
 
2757
 
 
2758
                var wasPaused = dom.wrapper.classList.contains( 'paused' );
 
2759
                dom.wrapper.classList.remove( 'paused' );
 
2760
 
 
2761
                cueAutoSlide();
 
2762
 
 
2763
                if( wasPaused ) {
 
2764
                        dispatchEvent( 'resumed' );
 
2765
                }
 
2766
 
 
2767
        }
 
2768
 
 
2769
        /**
 
2770
         * Toggles the paused mode on and off.
 
2771
         */
 
2772
        function togglePause( override ) {
 
2773
 
 
2774
                if( typeof override === 'boolean' ) {
 
2775
                        override ? pause() : resume();
 
2776
                }
 
2777
                else {
 
2778
                        isPaused() ? resume() : pause();
 
2779
                }
 
2780
 
 
2781
        }
 
2782
 
 
2783
        /**
 
2784
         * Checks if we are currently in the paused mode.
 
2785
         *
 
2786
         * @return {Boolean}
 
2787
         */
 
2788
        function isPaused() {
 
2789
 
 
2790
                return dom.wrapper.classList.contains( 'paused' );
 
2791
 
 
2792
        }
 
2793
 
 
2794
        /**
 
2795
         * Toggles the auto slide mode on and off.
 
2796
         *
 
2797
         * @param {Boolean} [override] Flag which sets the desired state.
 
2798
         * True means autoplay starts, false means it stops.
 
2799
         */
 
2800
 
 
2801
        function toggleAutoSlide( override ) {
 
2802
 
 
2803
                if( typeof override === 'boolean' ) {
 
2804
                        override ? resumeAutoSlide() : pauseAutoSlide();
 
2805
                }
 
2806
 
 
2807
                else {
 
2808
                        autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
 
2809
                }
 
2810
 
 
2811
        }
 
2812
 
 
2813
        /**
 
2814
         * Checks if the auto slide mode is currently on.
 
2815
         *
 
2816
         * @return {Boolean}
 
2817
         */
 
2818
        function isAutoSliding() {
 
2819
 
 
2820
                return !!( autoSlide && !autoSlidePaused );
 
2821
 
 
2822
        }
 
2823
 
 
2824
        /**
 
2825
         * Steps from the current point in the presentation to the
 
2826
         * slide which matches the specified horizontal and vertical
 
2827
         * indices.
 
2828
         *
 
2829
         * @param {number} [h=indexh] Horizontal index of the target slide
 
2830
         * @param {number} [v=indexv] Vertical index of the target slide
 
2831
         * @param {number} [f] Index of a fragment within the
 
2832
         * target slide to activate
 
2833
         * @param {number} [o] Origin for use in multimaster environments
 
2834
         */
 
2835
        function slide( h, v, f, o ) {
 
2836
 
 
2837
                // Remember where we were at before
 
2838
                previousSlide = currentSlide;
 
2839
 
 
2840
                // Query all horizontal slides in the deck
 
2841
                var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
 
2842
 
 
2843
                // Abort if there are no slides
 
2844
                if( horizontalSlides.length === 0 ) return;
 
2845
 
 
2846
                // If no vertical index is specified and the upcoming slide is a
 
2847
                // stack, resume at its previous vertical index
 
2848
                if( v === undefined && !isOverview() ) {
 
2849
                        v = getPreviousVerticalIndex( horizontalSlides[ h ] );
 
2850
                }
 
2851
 
 
2852
                // If we were on a vertical stack, remember what vertical index
 
2853
                // it was on so we can resume at the same position when returning
 
2854
                if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
 
2855
                        setPreviousVerticalIndex( previousSlide.parentNode, indexv );
 
2856
                }
 
2857
 
 
2858
                // Remember the state before this slide
 
2859
                var stateBefore = state.concat();
 
2860
 
 
2861
                // Reset the state array
 
2862
                state.length = 0;
 
2863
 
 
2864
                var indexhBefore = indexh || 0,
 
2865
                        indexvBefore = indexv || 0;
 
2866
 
 
2867
                // Activate and transition to the new slide
 
2868
                indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
 
2869
                indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
 
2870
 
 
2871
                // Update the visibility of slides now that the indices have changed
 
2872
                updateSlidesVisibility();
 
2873
 
 
2874
                layout();
 
2875
 
 
2876
                // Update the overview if it's currently active
 
2877
                if( isOverview() ) {
 
2878
                        updateOverview();
 
2879
                }
 
2880
 
 
2881
                // Find the current horizontal slide and any possible vertical slides
 
2882
                // within it
 
2883
                var currentHorizontalSlide = horizontalSlides[ indexh ],
 
2884
                        currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
 
2885
 
 
2886
                // Store references to the previous and current slides
 
2887
                currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
 
2888
 
 
2889
                // Show fragment, if specified
 
2890
                if( typeof f !== 'undefined' ) {
 
2891
                        navigateFragment( f );
 
2892
                }
 
2893
 
 
2894
                // Dispatch an event if the slide changed
 
2895
                var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
 
2896
                if (!slideChanged) {
 
2897
                        // Ensure that the previous slide is never the same as the current
 
2898
                        previousSlide = null;
 
2899
                }
 
2900
 
 
2901
                // Solves an edge case where the previous slide maintains the
 
2902
                // 'present' class when navigating between adjacent vertical
 
2903
                // stacks
 
2904
                if( previousSlide && previousSlide !== currentSlide ) {
 
2905
                        previousSlide.classList.remove( 'present' );
 
2906
                        previousSlide.setAttribute( 'aria-hidden', 'true' );
 
2907
 
 
2908
                        // Reset all slides upon navigate to home
 
2909
                        // Issue: #285
 
2910
                        if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
 
2911
                                // Launch async task
 
2912
                                setTimeout( function () {
 
2913
                                        var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
 
2914
                                        for( i in slides ) {
 
2915
                                                if( slides[i] ) {
 
2916
                                                        // Reset stack
 
2917
                                                        setPreviousVerticalIndex( slides[i], 0 );
 
2918
                                                }
 
2919
                                        }
 
2920
                                }, 0 );
 
2921
                        }
 
2922
                }
 
2923
 
 
2924
                // Apply the new state
 
2925
                stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
 
2926
                        // Check if this state existed on the previous slide. If it
 
2927
                        // did, we will avoid adding it repeatedly
 
2928
                        for( var j = 0; j < stateBefore.length; j++ ) {
 
2929
                                if( stateBefore[j] === state[i] ) {
 
2930
                                        stateBefore.splice( j, 1 );
 
2931
                                        continue stateLoop;
 
2932
                                }
 
2933
                        }
 
2934
 
 
2935
                        document.documentElement.classList.add( state[i] );
 
2936
 
 
2937
                        // Dispatch custom event matching the state's name
 
2938
                        dispatchEvent( state[i] );
 
2939
                }
 
2940
 
 
2941
                // Clean up the remains of the previous state
 
2942
                while( stateBefore.length ) {
 
2943
                        document.documentElement.classList.remove( stateBefore.pop() );
 
2944
                }
 
2945
 
 
2946
                if( slideChanged ) {
 
2947
                        dispatchEvent( 'slidechanged', {
 
2948
                                'indexh': indexh,
 
2949
                                'indexv': indexv,
 
2950
                                'previousSlide': previousSlide,
 
2951
                                'currentSlide': currentSlide,
 
2952
                                'origin': o
 
2953
                        } );
 
2954
                }
 
2955
 
 
2956
                // Handle embedded content
 
2957
                if( slideChanged || !previousSlide ) {
 
2958
                        stopEmbeddedContent( previousSlide );
 
2959
                        startEmbeddedContent( currentSlide );
 
2960
                }
 
2961
 
 
2962
                // Announce the current slide contents, for screen readers
 
2963
                dom.statusDiv.textContent = getStatusText( currentSlide );
 
2964
 
 
2965
                updateControls();
 
2966
                updateProgress();
 
2967
                updateBackground();
 
2968
                updateParallax();
 
2969
                updateSlideNumber();
 
2970
                updateNotes();
 
2971
                updateFragments();
 
2972
 
 
2973
                // Update the URL hash
 
2974
                writeURL();
 
2975
 
 
2976
                cueAutoSlide();
 
2977
 
 
2978
        }
 
2979
 
 
2980
        /**
 
2981
         * Syncs the presentation with the current DOM. Useful
 
2982
         * when new slides or control elements are added or when
 
2983
         * the configuration has changed.
 
2984
         */
 
2985
        function sync() {
 
2986
 
 
2987
                // Subscribe to input
 
2988
                removeEventListeners();
 
2989
                addEventListeners();
 
2990
 
 
2991
                // Force a layout to make sure the current config is accounted for
 
2992
                layout();
 
2993
 
 
2994
                // Reflect the current autoSlide value
 
2995
                autoSlide = config.autoSlide;
 
2996
 
 
2997
                // Start auto-sliding if it's enabled
 
2998
                cueAutoSlide();
 
2999
 
 
3000
                // Re-create the slide backgrounds
 
3001
                createBackgrounds();
 
3002
 
 
3003
                // Write the current hash to the URL
 
3004
                writeURL();
 
3005
 
 
3006
                sortAllFragments();
 
3007
 
 
3008
                updateControls();
 
3009
                updateProgress();
 
3010
                updateSlideNumber();
 
3011
                updateSlidesVisibility();
 
3012
                updateBackground( true );
 
3013
                updateNotesVisibility();
 
3014
                updateNotes();
 
3015
 
 
3016
                formatEmbeddedContent();
 
3017
 
 
3018
                // Start or stop embedded content depending on global config
 
3019
                if( config.autoPlayMedia === false ) {
 
3020
                        stopEmbeddedContent( currentSlide, { unloadIframes: false } );
 
3021
                }
 
3022
                else {
 
3023
                        startEmbeddedContent( currentSlide );
 
3024
                }
 
3025
 
 
3026
                if( isOverview() ) {
 
3027
                        layoutOverview();
 
3028
                }
 
3029
 
 
3030
        }
 
3031
 
 
3032
        /**
 
3033
         * Updates reveal.js to keep in sync with new slide attributes. For
 
3034
         * example, if you add a new `data-background-image` you can call
 
3035
         * this to have reveal.js render the new background image.
 
3036
         *
 
3037
         * Similar to #sync() but more efficient when you only need to
 
3038
         * refresh a specific slide.
 
3039
         *
 
3040
         * @param {HTMLElement} slide
 
3041
         */
 
3042
        function syncSlide( slide ) {
 
3043
 
 
3044
                // Default to the current slide
 
3045
                slide = slide || currentSlide;
 
3046
 
 
3047
                syncBackground( slide );
 
3048
                syncFragments( slide );
 
3049
 
 
3050
                updateBackground();
 
3051
                updateNotes();
 
3052
 
 
3053
                loadSlide( slide );
 
3054
 
 
3055
        }
 
3056
 
 
3057
        /**
 
3058
         * Formats the fragments on the given slide so that they have
 
3059
         * valid indices. Call this if fragments are changed in the DOM
 
3060
         * after reveal.js has already initialized.
 
3061
         *
 
3062
         * @param {HTMLElement} slide
 
3063
         * @return {Array} a list of the HTML fragments that were synced
 
3064
         */
 
3065
        function syncFragments( slide ) {
 
3066
 
 
3067
                // Default to the current slide
 
3068
                slide = slide || currentSlide;
 
3069
 
 
3070
                return sortFragments( slide.querySelectorAll( '.fragment' ) );
 
3071
 
 
3072
        }
 
3073
 
 
3074
        /**
 
3075
         * Resets all vertical slides so that only the first
 
3076
         * is visible.
 
3077
         */
 
3078
        function resetVerticalSlides() {
 
3079
 
 
3080
                var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
 
3081
                horizontalSlides.forEach( function( horizontalSlide ) {
 
3082
 
 
3083
                        var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
 
3084
                        verticalSlides.forEach( function( verticalSlide, y ) {
 
3085
 
 
3086
                                if( y > 0 ) {
 
3087
                                        verticalSlide.classList.remove( 'present' );
 
3088
                                        verticalSlide.classList.remove( 'past' );
 
3089
                                        verticalSlide.classList.add( 'future' );
 
3090
                                        verticalSlide.setAttribute( 'aria-hidden', 'true' );
 
3091
                                }
 
3092
 
 
3093
                        } );
 
3094
 
 
3095
                } );
 
3096
 
 
3097
        }
 
3098
 
 
3099
        /**
 
3100
         * Sorts and formats all of fragments in the
 
3101
         * presentation.
 
3102
         */
 
3103
        function sortAllFragments() {
 
3104
 
 
3105
                var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
 
3106
                horizontalSlides.forEach( function( horizontalSlide ) {
 
3107
 
 
3108
                        var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
 
3109
                        verticalSlides.forEach( function( verticalSlide, y ) {
 
3110
 
 
3111
                                sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
 
3112
 
 
3113
                        } );
 
3114
 
 
3115
                        if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
 
3116
 
 
3117
                } );
 
3118
 
 
3119
        }
 
3120
 
 
3121
        /**
 
3122
         * Randomly shuffles all slides in the deck.
 
3123
         */
 
3124
        function shuffle() {
 
3125
 
 
3126
                var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
 
3127
 
 
3128
                slides.forEach( function( slide ) {
 
3129
 
 
3130
                        // Insert this slide next to another random slide. This may
 
3131
                        // cause the slide to insert before itself but that's fine.
 
3132
                        dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
 
3133
 
 
3134
                } );
 
3135
 
 
3136
        }
 
3137
 
 
3138
        /**
 
3139
         * Updates one dimension of slides by showing the slide
 
3140
         * with the specified index.
 
3141
         *
 
3142
         * @param {string} selector A CSS selector that will fetch
 
3143
         * the group of slides we are working with
 
3144
         * @param {number} index The index of the slide that should be
 
3145
         * shown
 
3146
         *
 
3147
         * @return {number} The index of the slide that is now shown,
 
3148
         * might differ from the passed in index if it was out of
 
3149
         * bounds.
 
3150
         */
 
3151
        function updateSlides( selector, index ) {
 
3152
 
 
3153
                // Select all slides and convert the NodeList result to
 
3154
                // an array
 
3155
                var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
 
3156
                        slidesLength = slides.length;
 
3157
 
 
3158
                var printMode = isPrintingPDF();
 
3159
 
 
3160
                if( slidesLength ) {
 
3161
 
 
3162
                        // Should the index loop?
 
3163
                        if( config.loop ) {
 
3164
                                index %= slidesLength;
 
3165
 
 
3166
                                if( index < 0 ) {
 
3167
                                        index = slidesLength + index;
 
3168
                                }
 
3169
                        }
 
3170
 
 
3171
                        // Enforce max and minimum index bounds
 
3172
                        index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
 
3173
 
 
3174
                        for( var i = 0; i < slidesLength; i++ ) {
 
3175
                                var element = slides[i];
 
3176
 
 
3177
                                var reverse = config.rtl && !isVerticalSlide( element );
 
3178
 
 
3179
                                element.classList.remove( 'past' );
 
3180
                                element.classList.remove( 'present' );
 
3181
                                element.classList.remove( 'future' );
 
3182
 
 
3183
                                // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
 
3184
                                element.setAttribute( 'hidden', '' );
 
3185
                                element.setAttribute( 'aria-hidden', 'true' );
 
3186
 
 
3187
                                // If this element contains vertical slides
 
3188
                                if( element.querySelector( 'section' ) ) {
 
3189
                                        element.classList.add( 'stack' );
 
3190
                                }
 
3191
 
 
3192
                                // If we're printing static slides, all slides are "present"
 
3193
                                if( printMode ) {
 
3194
                                        element.classList.add( 'present' );
 
3195
                                        continue;
 
3196
                                }
 
3197
 
 
3198
                                if( i < index ) {
 
3199
                                        // Any element previous to index is given the 'past' class
 
3200
                                        element.classList.add( reverse ? 'future' : 'past' );
 
3201
 
 
3202
                                        if( config.fragments ) {
 
3203
                                                // Show all fragments in prior slides
 
3204
                                                toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
 
3205
                                                        fragment.classList.add( 'visible' );
 
3206
                                                        fragment.classList.remove( 'current-fragment' );
 
3207
                                                } );
 
3208
                                        }
 
3209
                                }
 
3210
                                else if( i > index ) {
 
3211
                                        // Any element subsequent to index is given the 'future' class
 
3212
                                        element.classList.add( reverse ? 'past' : 'future' );
 
3213
 
 
3214
                                        if( config.fragments ) {
 
3215
                                                // Hide all fragments in future slides
 
3216
                                                toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
 
3217
                                                        fragment.classList.remove( 'visible' );
 
3218
                                                        fragment.classList.remove( 'current-fragment' );
 
3219
                                                } );
 
3220
                                        }
 
3221
                                }
 
3222
                        }
 
3223
 
 
3224
                        // Mark the current slide as present
 
3225
                        slides[index].classList.add( 'present' );
 
3226
                        slides[index].removeAttribute( 'hidden' );
 
3227
                        slides[index].removeAttribute( 'aria-hidden' );
 
3228
 
 
3229
                        // If this slide has a state associated with it, add it
 
3230
                        // onto the current state of the deck
 
3231
                        var slideState = slides[index].getAttribute( 'data-state' );
 
3232
                        if( slideState ) {
 
3233
                                state = state.concat( slideState.split( ' ' ) );
 
3234
                        }
 
3235
 
 
3236
                }
 
3237
                else {
 
3238
                        // Since there are no slides we can't be anywhere beyond the
 
3239
                        // zeroth index
 
3240
                        index = 0;
 
3241
                }
 
3242
 
 
3243
                return index;
 
3244
 
 
3245
        }
 
3246
 
 
3247
        /**
 
3248
         * Optimization method; hide all slides that are far away
 
3249
         * from the present slide.
 
3250
         */
 
3251
        function updateSlidesVisibility() {
 
3252
 
 
3253
                // Select all slides and convert the NodeList result to
 
3254
                // an array
 
3255
                var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
 
3256
                        horizontalSlidesLength = horizontalSlides.length,
 
3257
                        distanceX,
 
3258
                        distanceY;
 
3259
 
 
3260
                if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
 
3261
 
 
3262
                        // The number of steps away from the present slide that will
 
3263
                        // be visible
 
3264
                        var viewDistance = isOverview() ? 10 : config.viewDistance;
 
3265
 
 
3266
                        // Limit view distance on weaker devices
 
3267
                        if( isMobileDevice ) {
 
3268
                                viewDistance = isOverview() ? 6 : 2;
 
3269
                        }
 
3270
 
 
3271
                        // All slides need to be visible when exporting to PDF
 
3272
                        if( isPrintingPDF() ) {
 
3273
                                viewDistance = Number.MAX_VALUE;
 
3274
                        }
 
3275
 
 
3276
                        for( var x = 0; x < horizontalSlidesLength; x++ ) {
 
3277
                                var horizontalSlide = horizontalSlides[x];
 
3278
 
 
3279
                                var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
 
3280
                                        verticalSlidesLength = verticalSlides.length;
 
3281
 
 
3282
                                // Determine how far away this slide is from the present
 
3283
                                distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
 
3284
 
 
3285
                                // If the presentation is looped, distance should measure
 
3286
                                // 1 between the first and last slides
 
3287
                                if( config.loop ) {
 
3288
                                        distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
 
3289
                                }
 
3290
 
 
3291
                                // Show the horizontal slide if it's within the view distance
 
3292
                                if( distanceX < viewDistance ) {
 
3293
                                        loadSlide( horizontalSlide );
 
3294
                                }
 
3295
                                else {
 
3296
                                        unloadSlide( horizontalSlide );
 
3297
                                }
 
3298
 
 
3299
                                if( verticalSlidesLength ) {
 
3300
 
 
3301
                                        var oy = getPreviousVerticalIndex( horizontalSlide );
 
3302
 
 
3303
                                        for( var y = 0; y < verticalSlidesLength; y++ ) {
 
3304
                                                var verticalSlide = verticalSlides[y];
 
3305
 
 
3306
                                                distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
 
3307
 
 
3308
                                                if( distanceX + distanceY < viewDistance ) {
 
3309
                                                        loadSlide( verticalSlide );
 
3310
                                                }
 
3311
                                                else {
 
3312
                                                        unloadSlide( verticalSlide );
 
3313
                                                }
 
3314
                                        }
 
3315
 
 
3316
                                }
 
3317
                        }
 
3318
 
 
3319
                        // Flag if there are ANY vertical slides, anywhere in the deck
 
3320
                        if( dom.wrapper.querySelectorAll( '.slides>section>section' ).length ) {
 
3321
                                dom.wrapper.classList.add( 'has-vertical-slides' );
 
3322
                        }
 
3323
                        else {
 
3324
                                dom.wrapper.classList.remove( 'has-vertical-slides' );
 
3325
                        }
 
3326
 
 
3327
                        // Flag if there are ANY horizontal slides, anywhere in the deck
 
3328
                        if( dom.wrapper.querySelectorAll( '.slides>section' ).length > 1 ) {
 
3329
                                dom.wrapper.classList.add( 'has-horizontal-slides' );
 
3330
                        }
 
3331
                        else {
 
3332
                                dom.wrapper.classList.remove( 'has-horizontal-slides' );
 
3333
                        }
 
3334
 
 
3335
                }
 
3336
 
 
3337
        }
 
3338
 
 
3339
        /**
 
3340
         * Pick up notes from the current slide and display them
 
3341
         * to the viewer.
 
3342
         *
 
3343
         * @see {@link config.showNotes}
 
3344
         */
 
3345
        function updateNotes() {
 
3346
 
 
3347
                if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
 
3348
 
 
3349
                        dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
 
3350
 
 
3351
                }
 
3352
 
 
3353
        }
 
3354
 
 
3355
        /**
 
3356
         * Updates the visibility of the speaker notes sidebar that
 
3357
         * is used to share annotated slides. The notes sidebar is
 
3358
         * only visible if showNotes is true and there are notes on
 
3359
         * one or more slides in the deck.
 
3360
         */
 
3361
        function updateNotesVisibility() {
 
3362
 
 
3363
                if( config.showNotes && hasNotes() ) {
 
3364
                        dom.wrapper.classList.add( 'show-notes' );
 
3365
                }
 
3366
                else {
 
3367
                        dom.wrapper.classList.remove( 'show-notes' );
 
3368
                }
 
3369
 
 
3370
        }
 
3371
 
 
3372
        /**
 
3373
         * Checks if there are speaker notes for ANY slide in the
 
3374
         * presentation.
 
3375
         */
 
3376
        function hasNotes() {
 
3377
 
 
3378
                return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0;
 
3379
 
 
3380
        }
 
3381
 
 
3382
        /**
 
3383
         * Updates the progress bar to reflect the current slide.
 
3384
         */
 
3385
        function updateProgress() {
 
3386
 
 
3387
                // Update progress if enabled
 
3388
                if( config.progress && dom.progressbar ) {
 
3389
 
 
3390
                        dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
 
3391
 
 
3392
                }
 
3393
 
 
3394
        }
 
3395
 
 
3396
 
 
3397
        /**
 
3398
         * Updates the slide number to match the current slide.
 
3399
         */
 
3400
        function updateSlideNumber() {
 
3401
 
 
3402
                // Update slide number if enabled
 
3403
                if( config.slideNumber && dom.slideNumber ) {
 
3404
 
 
3405
                        var value;
 
3406
                        var format = 'h.v';
 
3407
 
 
3408
                        if( typeof config.slideNumber === 'function' ) {
 
3409
                                value = config.slideNumber();
 
3410
                        }
 
3411
                        else {
 
3412
                                // Check if a custom number format is available
 
3413
                                if( typeof config.slideNumber === 'string' ) {
 
3414
                                        format = config.slideNumber;
 
3415
                                }
 
3416
 
 
3417
                                // If there are ONLY vertical slides in this deck, always use
 
3418
                                // a flattened slide number
 
3419
                                if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
 
3420
                                        format = 'c';
 
3421
                                }
 
3422
 
 
3423
                                value = [];
 
3424
                                switch( format ) {
 
3425
                                        case 'c':
 
3426
                                                value.push( getSlidePastCount() + 1 );
 
3427
                                                break;
 
3428
                                        case 'c/t':
 
3429
                                                value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
 
3430
                                                break;
 
3431
                                        case 'h/v':
 
3432
                                                value.push( indexh + 1 );
 
3433
                                                if( isVerticalSlide() ) value.push( '/', indexv + 1 );
 
3434
                                                break;
 
3435
                                        default:
 
3436
                                                value.push( indexh + 1 );
 
3437
                                                if( isVerticalSlide() ) value.push( '.', indexv + 1 );
 
3438
                                }
 
3439
                        }
 
3440
 
 
3441
                        dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
 
3442
                }
 
3443
 
 
3444
        }
 
3445
 
 
3446
        /**
 
3447
         * Applies HTML formatting to a slide number before it's
 
3448
         * written to the DOM.
 
3449
         *
 
3450
         * @param {number} a Current slide
 
3451
         * @param {string} delimiter Character to separate slide numbers
 
3452
         * @param {(number|*)} b Total slides
 
3453
         * @return {string} HTML string fragment
 
3454
         */
 
3455
        function formatSlideNumber( a, delimiter, b ) {
 
3456
 
 
3457
                var url = '#' + locationHash();
 
3458
                if( typeof b === 'number' && !isNaN( b ) ) {
 
3459
                        return  '<a href="' + url + '">' +
 
3460
                                        '<span class="slide-number-a">'+ a +'</span>' +
 
3461
                                        '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
 
3462
                                        '<span class="slide-number-b">'+ b +'</span>' +
 
3463
                                        '</a>';
 
3464
                }
 
3465
                else {
 
3466
                        return '<a href="' + url + '">' +
 
3467
                               '<span class="slide-number-a">'+ a +'</span>' +
 
3468
                               '</a>';
 
3469
                }
 
3470
 
 
3471
        }
 
3472
 
 
3473
        /**
 
3474
         * Updates the state of all control/navigation arrows.
 
3475
         */
 
3476
        function updateControls() {
 
3477
 
 
3478
                var routes = availableRoutes();
 
3479
                var fragments = availableFragments();
 
3480
 
 
3481
                // Remove the 'enabled' class from all directions
 
3482
                dom.controlsLeft.concat( dom.controlsRight )
 
3483
                                                .concat( dom.controlsUp )
 
3484
                                                .concat( dom.controlsDown )
 
3485
                                                .concat( dom.controlsPrev )
 
3486
                                                .concat( dom.controlsNext ).forEach( function( node ) {
 
3487
                        node.classList.remove( 'enabled' );
 
3488
                        node.classList.remove( 'fragmented' );
 
3489
 
 
3490
                        // Set 'disabled' attribute on all directions
 
3491
                        node.setAttribute( 'disabled', 'disabled' );
 
3492
                } );
 
3493
 
 
3494
                // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
 
3495
                if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3496
                if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3497
                if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3498
                if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3499
 
 
3500
                // Prev/next buttons
 
3501
                if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3502
                if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3503
 
 
3504
                // Highlight fragment directions
 
3505
                if( currentSlide ) {
 
3506
 
 
3507
                        // Always apply fragment decorator to prev/next buttons
 
3508
                        if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3509
                        if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3510
 
 
3511
                        // Apply fragment decorators to directional buttons based on
 
3512
                        // what slide axis they are in
 
3513
                        if( isVerticalSlide( currentSlide ) ) {
 
3514
                                if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3515
                                if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3516
                        }
 
3517
                        else {
 
3518
                                if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3519
                                if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
 
3520
                        }
 
3521
 
 
3522
                }
 
3523
 
 
3524
                if( config.controlsTutorial ) {
 
3525
 
 
3526
                        // Highlight control arrows with an animation to ensure
 
3527
                        // that the viewer knows how to navigate
 
3528
                        if( !hasNavigatedDown && routes.down ) {
 
3529
                                dom.controlsDownArrow.classList.add( 'highlight' );
 
3530
                        }
 
3531
                        else {
 
3532
                                dom.controlsDownArrow.classList.remove( 'highlight' );
 
3533
 
 
3534
                                if( !hasNavigatedRight && routes.right && indexv === 0 ) {
 
3535
                                        dom.controlsRightArrow.classList.add( 'highlight' );
 
3536
                                }
 
3537
                                else {
 
3538
                                        dom.controlsRightArrow.classList.remove( 'highlight' );
 
3539
                                }
 
3540
                        }
 
3541
 
 
3542
                }
 
3543
 
 
3544
        }
 
3545
 
 
3546
        /**
 
3547
         * Updates the background elements to reflect the current
 
3548
         * slide.
 
3549
         *
 
3550
         * @param {boolean} includeAll If true, the backgrounds of
 
3551
         * all vertical slides (not just the present) will be updated.
 
3552
         */
 
3553
        function updateBackground( includeAll ) {
 
3554
 
 
3555
                var currentBackground = null;
 
3556
 
 
3557
                // Reverse past/future classes when in RTL mode
 
3558
                var horizontalPast = config.rtl ? 'future' : 'past',
 
3559
                        horizontalFuture = config.rtl ? 'past' : 'future';
 
3560
 
 
3561
                // Update the classes of all backgrounds to match the
 
3562
                // states of their slides (past/present/future)
 
3563
                toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
 
3564
 
 
3565
                        backgroundh.classList.remove( 'past' );
 
3566
                        backgroundh.classList.remove( 'present' );
 
3567
                        backgroundh.classList.remove( 'future' );
 
3568
 
 
3569
                        if( h < indexh ) {
 
3570
                                backgroundh.classList.add( horizontalPast );
 
3571
                        }
 
3572
                        else if ( h > indexh ) {
 
3573
                                backgroundh.classList.add( horizontalFuture );
 
3574
                        }
 
3575
                        else {
 
3576
                                backgroundh.classList.add( 'present' );
 
3577
 
 
3578
                                // Store a reference to the current background element
 
3579
                                currentBackground = backgroundh;
 
3580
                        }
 
3581
 
 
3582
                        if( includeAll || h === indexh ) {
 
3583
                                toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
 
3584
 
 
3585
                                        backgroundv.classList.remove( 'past' );
 
3586
                                        backgroundv.classList.remove( 'present' );
 
3587
                                        backgroundv.classList.remove( 'future' );
 
3588
 
 
3589
                                        if( v < indexv ) {
 
3590
                                                backgroundv.classList.add( 'past' );
 
3591
                                        }
 
3592
                                        else if ( v > indexv ) {
 
3593
                                                backgroundv.classList.add( 'future' );
 
3594
                                        }
 
3595
                                        else {
 
3596
                                                backgroundv.classList.add( 'present' );
 
3597
 
 
3598
                                                // Only if this is the present horizontal and vertical slide
 
3599
                                                if( h === indexh ) currentBackground = backgroundv;
 
3600
                                        }
 
3601
 
 
3602
                                } );
 
3603
                        }
 
3604
 
 
3605
                } );
 
3606
 
 
3607
                // Stop content inside of previous backgrounds
 
3608
                if( previousBackground ) {
 
3609
 
 
3610
                        stopEmbeddedContent( previousBackground );
 
3611
 
 
3612
                }
 
3613
 
 
3614
                // Start content in the current background
 
3615
                if( currentBackground ) {
 
3616
 
 
3617
                        startEmbeddedContent( currentBackground );
 
3618
 
 
3619
                        var currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
 
3620
                        if( currentBackgroundContent ) {
 
3621
 
 
3622
                                var backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
 
3623
 
 
3624
                                // Restart GIFs (doesn't work in Firefox)
 
3625
                                if( /\.gif/i.test( backgroundImageURL ) ) {
 
3626
                                        currentBackgroundContent.style.backgroundImage = '';
 
3627
                                        window.getComputedStyle( currentBackgroundContent ).opacity;
 
3628
                                        currentBackgroundContent.style.backgroundImage = backgroundImageURL;
 
3629
                                }
 
3630
 
 
3631
                        }
 
3632
 
 
3633
                        // Don't transition between identical backgrounds. This
 
3634
                        // prevents unwanted flicker.
 
3635
                        var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
 
3636
                        var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
 
3637
                        if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
 
3638
                                dom.background.classList.add( 'no-transition' );
 
3639
                        }
 
3640
 
 
3641
                        previousBackground = currentBackground;
 
3642
 
 
3643
                }
 
3644
 
 
3645
                // If there's a background brightness flag for this slide,
 
3646
                // bubble it to the .reveal container
 
3647
                if( currentSlide ) {
 
3648
                        [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
 
3649
                                if( currentSlide.classList.contains( classToBubble ) ) {
 
3650
                                        dom.wrapper.classList.add( classToBubble );
 
3651
                                }
 
3652
                                else {
 
3653
                                        dom.wrapper.classList.remove( classToBubble );
 
3654
                                }
 
3655
                        } );
 
3656
                }
 
3657
 
 
3658
                // Allow the first background to apply without transition
 
3659
                setTimeout( function() {
 
3660
                        dom.background.classList.remove( 'no-transition' );
 
3661
                }, 1 );
 
3662
 
 
3663
        }
 
3664
 
 
3665
        /**
 
3666
         * Updates the position of the parallax background based
 
3667
         * on the current slide index.
 
3668
         */
 
3669
        function updateParallax() {
 
3670
 
 
3671
                if( config.parallaxBackgroundImage ) {
 
3672
 
 
3673
                        var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
 
3674
                                verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
 
3675
 
 
3676
                        var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
 
3677
                                backgroundWidth, backgroundHeight;
 
3678
 
 
3679
                        if( backgroundSize.length === 1 ) {
 
3680
                                backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
 
3681
                        }
 
3682
                        else {
 
3683
                                backgroundWidth = parseInt( backgroundSize[0], 10 );
 
3684
                                backgroundHeight = parseInt( backgroundSize[1], 10 );
 
3685
                        }
 
3686
 
 
3687
                        var slideWidth = dom.background.offsetWidth,
 
3688
                                horizontalSlideCount = horizontalSlides.length,
 
3689
                                horizontalOffsetMultiplier,
 
3690
                                horizontalOffset;
 
3691
 
 
3692
                        if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
 
3693
                                horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
 
3694
                        }
 
3695
                        else {
 
3696
                                horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
 
3697
                        }
 
3698
 
 
3699
                        horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
 
3700
 
 
3701
                        var slideHeight = dom.background.offsetHeight,
 
3702
                                verticalSlideCount = verticalSlides.length,
 
3703
                                verticalOffsetMultiplier,
 
3704
                                verticalOffset;
 
3705
 
 
3706
                        if( typeof config.parallaxBackgroundVertical === 'number' ) {
 
3707
                                verticalOffsetMultiplier = config.parallaxBackgroundVertical;
 
3708
                        }
 
3709
                        else {
 
3710
                                verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
 
3711
                        }
 
3712
 
 
3713
                        verticalOffset = verticalSlideCount > 0 ?  verticalOffsetMultiplier * indexv : 0;
 
3714
 
 
3715
                        dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
 
3716
 
 
3717
                }
 
3718
 
 
3719
        }
 
3720
 
 
3721
        /**
 
3722
         * Should the given element be preloaded?
 
3723
         * Decides based on local element attributes and global config.
 
3724
         *
 
3725
         * @param {HTMLElement} element
 
3726
         */
 
3727
        function shouldPreload( element ) {
 
3728
 
 
3729
                // Prefer an explicit global preload setting
 
3730
                var preload = config.preloadIframes;
 
3731
 
 
3732
                // If no global setting is available, fall back on the element's
 
3733
                // own preload setting
 
3734
                if( typeof preload !== 'boolean' ) {
 
3735
                        preload = element.hasAttribute( 'data-preload' );
 
3736
                }
 
3737
 
 
3738
                return preload;
 
3739
        }
 
3740
 
 
3741
        /**
 
3742
         * Called when the given slide is within the configured view
 
3743
         * distance. Shows the slide element and loads any content
 
3744
         * that is set to load lazily (data-src).
 
3745
         *
 
3746
         * @param {HTMLElement} slide Slide to show
 
3747
         */
 
3748
        function loadSlide( slide, options ) {
 
3749
 
 
3750
                options = options || {};
 
3751
 
 
3752
                // Show the slide element
 
3753
                slide.style.display = config.display;
 
3754
 
 
3755
                // Media elements with data-src attributes
 
3756
                toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
 
3757
                        if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
 
3758
                                element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
 
3759
                                element.setAttribute( 'data-lazy-loaded', '' );
 
3760
                                element.removeAttribute( 'data-src' );
 
3761
                        }
 
3762
                } );
 
3763
 
 
3764
                // Media elements with <source> children
 
3765
                toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
 
3766
                        var sources = 0;
 
3767
 
 
3768
                        toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
 
3769
                                source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
 
3770
                                source.removeAttribute( 'data-src' );
 
3771
                                source.setAttribute( 'data-lazy-loaded', '' );
 
3772
                                sources += 1;
 
3773
                        } );
 
3774
 
 
3775
                        // If we rewrote sources for this video/audio element, we need
 
3776
                        // to manually tell it to load from its new origin
 
3777
                        if( sources > 0 ) {
 
3778
                                media.load();
 
3779
                        }
 
3780
                } );
 
3781
 
 
3782
 
 
3783
                // Show the corresponding background element
 
3784
                var background = slide.slideBackgroundElement;
 
3785
                if( background ) {
 
3786
                        background.style.display = 'block';
 
3787
 
 
3788
                        var backgroundContent = slide.slideBackgroundContentElement;
 
3789
 
 
3790
                        // If the background contains media, load it
 
3791
                        if( background.hasAttribute( 'data-loaded' ) === false ) {
 
3792
                                background.setAttribute( 'data-loaded', 'true' );
 
3793
 
 
3794
                                var backgroundImage = slide.getAttribute( 'data-background-image' ),
 
3795
                                        backgroundVideo = slide.getAttribute( 'data-background-video' ),
 
3796
                                        backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
 
3797
                                        backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
 
3798
                                        backgroundIframe = slide.getAttribute( 'data-background-iframe' );
 
3799
 
 
3800
                                // Images
 
3801
                                if( backgroundImage ) {
 
3802
                                        backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')';
 
3803
                                }
 
3804
                                // Videos
 
3805
                                else if ( backgroundVideo && !isSpeakerNotes() ) {
 
3806
                                        var video = document.createElement( 'video' );
 
3807
 
 
3808
                                        if( backgroundVideoLoop ) {
 
3809
                                                video.setAttribute( 'loop', '' );
 
3810
                                        }
 
3811
 
 
3812
                                        if( backgroundVideoMuted ) {
 
3813
                                                video.muted = true;
 
3814
                                        }
 
3815
 
 
3816
                                        // Inline video playback works (at least in Mobile Safari) as
 
3817
                                        // long as the video is muted and the `playsinline` attribute is
 
3818
                                        // present
 
3819
                                        if( isMobileDevice ) {
 
3820
                                                video.muted = true;
 
3821
                                                video.autoplay = true;
 
3822
                                                video.setAttribute( 'playsinline', '' );
 
3823
                                        }
 
3824
 
 
3825
                                        // Support comma separated lists of video sources
 
3826
                                        backgroundVideo.split( ',' ).forEach( function( source ) {
 
3827
                                                video.innerHTML += '<source src="'+ source +'">';
 
3828
                                        } );
 
3829
 
 
3830
                                        backgroundContent.appendChild( video );
 
3831
                                }
 
3832
                                // Iframes
 
3833
                                else if( backgroundIframe && options.excludeIframes !== true ) {
 
3834
                                        var iframe = document.createElement( 'iframe' );
 
3835
                                        iframe.setAttribute( 'allowfullscreen', '' );
 
3836
                                        iframe.setAttribute( 'mozallowfullscreen', '' );
 
3837
                                        iframe.setAttribute( 'webkitallowfullscreen', '' );
 
3838
 
 
3839
                                        // Only load autoplaying content when the slide is shown to
 
3840
                                        // avoid having it play in the background
 
3841
                                        if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
 
3842
                                                iframe.setAttribute( 'data-src', backgroundIframe );
 
3843
                                        }
 
3844
                                        else {
 
3845
                                                iframe.setAttribute( 'src', backgroundIframe );
 
3846
                                        }
 
3847
 
 
3848
                                        iframe.style.width  = '100%';
 
3849
                                        iframe.style.height = '100%';
 
3850
                                        iframe.style.maxHeight = '100%';
 
3851
                                        iframe.style.maxWidth = '100%';
 
3852
 
 
3853
                                        backgroundContent.appendChild( iframe );
 
3854
                                }
 
3855
                        }
 
3856
 
 
3857
                }
 
3858
 
 
3859
        }
 
3860
 
 
3861
        /**
 
3862
         * Unloads and hides the given slide. This is called when the
 
3863
         * slide is moved outside of the configured view distance.
 
3864
         *
 
3865
         * @param {HTMLElement} slide
 
3866
         */
 
3867
        function unloadSlide( slide ) {
 
3868
 
 
3869
                // Hide the slide element
 
3870
                slide.style.display = 'none';
 
3871
 
 
3872
                // Hide the corresponding background element
 
3873
                var background = getSlideBackground( slide );
 
3874
                if( background ) {
 
3875
                        background.style.display = 'none';
 
3876
                }
 
3877
 
 
3878
                // Reset lazy-loaded media elements with src attributes
 
3879
                toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
 
3880
                        element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
 
3881
                        element.removeAttribute( 'src' );
 
3882
                } );
 
3883
 
 
3884
                // Reset lazy-loaded media elements with <source> children
 
3885
                toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) {
 
3886
                        source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
 
3887
                        source.removeAttribute( 'src' );
 
3888
                } );
 
3889
 
 
3890
        }
 
3891
 
 
3892
        /**
 
3893
         * Determine what available routes there are for navigation.
 
3894
         *
 
3895
         * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
 
3896
         */
 
3897
        function availableRoutes() {
 
3898
 
 
3899
                var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
 
3900
                        verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
 
3901
 
 
3902
                var routes = {
 
3903
                        left: indexh > 0,
 
3904
                        right: indexh < horizontalSlides.length - 1,
 
3905
                        up: indexv > 0,
 
3906
                        down: indexv < verticalSlides.length - 1
 
3907
                };
 
3908
 
 
3909
                // Looped presentations can always be navigated as long as
 
3910
                // there are slides available
 
3911
                if( config.loop ) {
 
3912
                        if( horizontalSlides.length > 1 ) {
 
3913
                                routes.left = true;
 
3914
                                routes.right = true;
 
3915
                        }
 
3916
 
 
3917
                        if( verticalSlides.length > 1 ) {
 
3918
                                routes.up = true;
 
3919
                                routes.down = true;
 
3920
                        }
 
3921
                }
 
3922
 
 
3923
                // Reverse horizontal controls for rtl
 
3924
                if( config.rtl ) {
 
3925
                        var left = routes.left;
 
3926
                        routes.left = routes.right;
 
3927
                        routes.right = left;
 
3928
                }
 
3929
 
 
3930
                return routes;
 
3931
 
 
3932
        }
 
3933
 
 
3934
        /**
 
3935
         * Returns an object describing the available fragment
 
3936
         * directions.
 
3937
         *
 
3938
         * @return {{prev: boolean, next: boolean}}
 
3939
         */
 
3940
        function availableFragments() {
 
3941
 
 
3942
                if( currentSlide && config.fragments ) {
 
3943
                        var fragments = currentSlide.querySelectorAll( '.fragment' );
 
3944
                        var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
 
3945
 
 
3946
                        return {
 
3947
                                prev: fragments.length - hiddenFragments.length > 0,
 
3948
                                next: !!hiddenFragments.length
 
3949
                        };
 
3950
                }
 
3951
                else {
 
3952
                        return { prev: false, next: false };
 
3953
                }
 
3954
 
 
3955
        }
 
3956
 
 
3957
        /**
 
3958
         * Enforces origin-specific format rules for embedded media.
 
3959
         */
 
3960
        function formatEmbeddedContent() {
 
3961
 
 
3962
                var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
 
3963
                        toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
 
3964
                                var src = el.getAttribute( sourceAttribute );
 
3965
                                if( src && src.indexOf( param ) === -1 ) {
 
3966
                                        el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
 
3967
                                }
 
3968
                        });
 
3969
                };
 
3970
 
 
3971
                // YouTube frames must include "?enablejsapi=1"
 
3972
                _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
 
3973
                _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
 
3974
 
 
3975
                // Vimeo frames must include "?api=1"
 
3976
                _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
 
3977
                _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
 
3978
 
 
3979
        }
 
3980
 
 
3981
        /**
 
3982
         * Start playback of any embedded content inside of
 
3983
         * the given element.
 
3984
         *
 
3985
         * @param {HTMLElement} element
 
3986
         */
 
3987
        function startEmbeddedContent( element ) {
 
3988
 
 
3989
                if( element && !isSpeakerNotes() ) {
 
3990
 
 
3991
                        // Restart GIFs
 
3992
                        toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
 
3993
                                // Setting the same unchanged source like this was confirmed
 
3994
                                // to work in Chrome, FF & Safari
 
3995
                                el.setAttribute( 'src', el.getAttribute( 'src' ) );
 
3996
                        } );
 
3997
 
 
3998
                        // HTML5 media elements
 
3999
                        toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
 
4000
                                if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
 
4001
                                        return;
 
4002
                                }
 
4003
 
 
4004
                                // Prefer an explicit global autoplay setting
 
4005
                                var autoplay = config.autoPlayMedia;
 
4006
 
 
4007
                                // If no global setting is available, fall back on the element's
 
4008
                                // own autoplay setting
 
4009
                                if( typeof autoplay !== 'boolean' ) {
 
4010
                                        autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
 
4011
                                }
 
4012
 
 
4013
                                if( autoplay && typeof el.play === 'function' ) {
 
4014
 
 
4015
                                        // If the media is ready, start playback
 
4016
                                        if( el.readyState > 1 ) {
 
4017
                                                startEmbeddedMedia( { target: el } );
 
4018
                                        }
 
4019
                                        // Mobile devices never fire a loaded event so instead
 
4020
                                        // of waiting, we initiate playback
 
4021
                                        else if( isMobileDevice ) {
 
4022
                                                var promise = el.play();
 
4023
 
 
4024
                                                // If autoplay does not work, ensure that the controls are visible so
 
4025
                                                // that the viewer can start the media on their own
 
4026
                                                if( promise && typeof promise.catch === 'function' && el.controls === false ) {
 
4027
                                                        promise.catch( function() {
 
4028
                                                                el.controls = true;
 
4029
 
 
4030
                                                                // Once the video does start playing, hide the controls again
 
4031
                                                                el.addEventListener( 'play', function() {
 
4032
                                                                        el.controls = false;
 
4033
                                                                } );
 
4034
                                                        } );
 
4035
                                                }
 
4036
                                        }
 
4037
                                        // If the media isn't loaded, wait before playing
 
4038
                                        else {
 
4039
                                                el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
 
4040
                                                el.addEventListener( 'loadeddata', startEmbeddedMedia );
 
4041
                                        }
 
4042
 
 
4043
                                }
 
4044
                        } );
 
4045
 
 
4046
                        // Normal iframes
 
4047
                        toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
 
4048
                                if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
 
4049
                                        return;
 
4050
                                }
 
4051
 
 
4052
                                startEmbeddedIframe( { target: el } );
 
4053
                        } );
 
4054
 
 
4055
                        // Lazy loading iframes
 
4056
                        toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
 
4057
                                if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
 
4058
                                        return;
 
4059
                                }
 
4060
 
 
4061
                                if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
 
4062
                                        el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
 
4063
                                        el.addEventListener( 'load', startEmbeddedIframe );
 
4064
                                        el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
 
4065
                                }
 
4066
                        } );
 
4067
 
 
4068
                }
 
4069
 
 
4070
        }
 
4071
 
 
4072
        /**
 
4073
         * Starts playing an embedded video/audio element after
 
4074
         * it has finished loading.
 
4075
         *
 
4076
         * @param {object} event
 
4077
         */
 
4078
        function startEmbeddedMedia( event ) {
 
4079
 
 
4080
                var isAttachedToDOM = !!closestParent( event.target, 'html' ),
 
4081
                        isVisible               = !!closestParent( event.target, '.present' );
 
4082
 
 
4083
                if( isAttachedToDOM && isVisible ) {
 
4084
                        event.target.currentTime = 0;
 
4085
                        event.target.play();
 
4086
                }
 
4087
 
 
4088
                event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
 
4089
 
 
4090
        }
 
4091
 
 
4092
        /**
 
4093
         * "Starts" the content of an embedded iframe using the
 
4094
         * postMessage API.
 
4095
         *
 
4096
         * @param {object} event
 
4097
         */
 
4098
        function startEmbeddedIframe( event ) {
 
4099
 
 
4100
                var iframe = event.target;
 
4101
 
 
4102
                if( iframe && iframe.contentWindow ) {
 
4103
 
 
4104
                        var isAttachedToDOM = !!closestParent( event.target, 'html' ),
 
4105
                                isVisible               = !!closestParent( event.target, '.present' );
 
4106
 
 
4107
                        if( isAttachedToDOM && isVisible ) {
 
4108
 
 
4109
                                // Prefer an explicit global autoplay setting
 
4110
                                var autoplay = config.autoPlayMedia;
 
4111
 
 
4112
                                // If no global setting is available, fall back on the element's
 
4113
                                // own autoplay setting
 
4114
                                if( typeof autoplay !== 'boolean' ) {
 
4115
                                        autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
 
4116
                                }
 
4117
 
 
4118
                                // YouTube postMessage API
 
4119
                                if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
 
4120
                                        iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
 
4121
                                }
 
4122
                                // Vimeo postMessage API
 
4123
                                else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
 
4124
                                        iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
 
4125
                                }
 
4126
                                // Generic postMessage API
 
4127
                                else {
 
4128
                                        iframe.contentWindow.postMessage( 'slide:start', '*' );
 
4129
                                }
 
4130
 
 
4131
                        }
 
4132
 
 
4133
                }
 
4134
 
 
4135
        }
 
4136
 
 
4137
        /**
 
4138
         * Stop playback of any embedded content inside of
 
4139
         * the targeted slide.
 
4140
         *
 
4141
         * @param {HTMLElement} element
 
4142
         */
 
4143
        function stopEmbeddedContent( element, options ) {
 
4144
 
 
4145
                options = extend( {
 
4146
                        // Defaults
 
4147
                        unloadIframes: true
 
4148
                }, options || {} );
 
4149
 
 
4150
                if( element && element.parentNode ) {
 
4151
                        // HTML5 media elements
 
4152
                        toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
 
4153
                                if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
 
4154
                                        el.setAttribute('data-paused-by-reveal', '');
 
4155
                                        el.pause();
 
4156
                                }
 
4157
                        } );
 
4158
 
 
4159
                        // Generic postMessage API for non-lazy loaded iframes
 
4160
                        toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
 
4161
                                if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
 
4162
                                el.removeEventListener( 'load', startEmbeddedIframe );
 
4163
                        });
 
4164
 
 
4165
                        // YouTube postMessage API
 
4166
                        toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
 
4167
                                if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
 
4168
                                        el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
 
4169
                                }
 
4170
                        });
 
4171
 
 
4172
                        // Vimeo postMessage API
 
4173
                        toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
 
4174
                                if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
 
4175
                                        el.contentWindow.postMessage( '{"method":"pause"}', '*' );
 
4176
                                }
 
4177
                        });
 
4178
 
 
4179
                        if( options.unloadIframes === true ) {
 
4180
                                // Unload lazy-loaded iframes
 
4181
                                toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
 
4182
                                        // Only removing the src doesn't actually unload the frame
 
4183
                                        // in all browsers (Firefox) so we set it to blank first
 
4184
                                        el.setAttribute( 'src', 'about:blank' );
 
4185
                                        el.removeAttribute( 'src' );
 
4186
                                } );
 
4187
                        }
 
4188
                }
 
4189
 
 
4190
        }
 
4191
 
 
4192
        /**
 
4193
         * Returns the number of past slides. This can be used as a global
 
4194
         * flattened index for slides.
 
4195
         *
 
4196
         * @return {number} Past slide count
 
4197
         */
 
4198
        function getSlidePastCount() {
 
4199
 
 
4200
                var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
 
4201
 
 
4202
                // The number of past slides
 
4203
                var pastCount = 0;
 
4204
 
 
4205
                // Step through all slides and count the past ones
 
4206
                mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
 
4207
 
 
4208
                        var horizontalSlide = horizontalSlides[i];
 
4209
                        var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
 
4210
 
 
4211
                        for( var j = 0; j < verticalSlides.length; j++ ) {
 
4212
 
 
4213
                                // Stop as soon as we arrive at the present
 
4214
                                if( verticalSlides[j].classList.contains( 'present' ) ) {
 
4215
                                        break mainLoop;
 
4216
                                }
 
4217
 
 
4218
                                pastCount++;
 
4219
 
 
4220
                        }
 
4221
 
 
4222
                        // Stop as soon as we arrive at the present
 
4223
                        if( horizontalSlide.classList.contains( 'present' ) ) {
 
4224
                                break;
 
4225
                        }
 
4226
 
 
4227
                        // Don't count the wrapping section for vertical slides
 
4228
                        if( horizontalSlide.classList.contains( 'stack' ) === false ) {
 
4229
                                pastCount++;
 
4230
                        }
 
4231
 
 
4232
                }
 
4233
 
 
4234
                return pastCount;
 
4235
 
 
4236
        }
 
4237
 
 
4238
        /**
 
4239
         * Returns a value ranging from 0-1 that represents
 
4240
         * how far into the presentation we have navigated.
 
4241
         *
 
4242
         * @return {number}
 
4243
         */
 
4244
        function getProgress() {
 
4245
 
 
4246
                // The number of past and total slides
 
4247
                var totalCount = getTotalSlides();
 
4248
                var pastCount = getSlidePastCount();
 
4249
 
 
4250
                if( currentSlide ) {
 
4251
 
 
4252
                        var allFragments = currentSlide.querySelectorAll( '.fragment' );
 
4253
 
 
4254
                        // If there are fragments in the current slide those should be
 
4255
                        // accounted for in the progress.
 
4256
                        if( allFragments.length > 0 ) {
 
4257
                                var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
 
4258
 
 
4259
                                // This value represents how big a portion of the slide progress
 
4260
                                // that is made up by its fragments (0-1)
 
4261
                                var fragmentWeight = 0.9;
 
4262
 
 
4263
                                // Add fragment progress to the past slide count
 
4264
                                pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
 
4265
                        }
 
4266
 
 
4267
                }
 
4268
 
 
4269
                return Math.min( pastCount / ( totalCount - 1 ), 1 );
 
4270
 
 
4271
        }
 
4272
 
 
4273
        /**
 
4274
         * Checks if this presentation is running inside of the
 
4275
         * speaker notes window.
 
4276
         *
 
4277
         * @return {boolean}
 
4278
         */
 
4279
        function isSpeakerNotes() {
 
4280
 
 
4281
                return !!window.location.search.match( /receiver/gi );
 
4282
 
 
4283
        }
 
4284
 
 
4285
        /**
 
4286
         * Reads the current URL (hash) and navigates accordingly.
 
4287
         */
 
4288
        function readURL() {
 
4289
 
 
4290
                var hash = window.location.hash;
 
4291
 
 
4292
                // Attempt to parse the hash as either an index or name
 
4293
                var bits = hash.slice( 2 ).split( '/' ),
 
4294
                        name = hash.replace( /#|\//gi, '' );
 
4295
 
 
4296
                // If the first bit is not fully numeric and there is a name we
 
4297
                // can assume that this is a named link
 
4298
                if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
 
4299
                        var element;
 
4300
 
 
4301
                        // Ensure the named link is a valid HTML ID attribute
 
4302
                        try {
 
4303
                                element = document.getElementById( decodeURIComponent( name ) );
 
4304
                        }
 
4305
                        catch ( error ) { }
 
4306
 
 
4307
                        // Ensure that we're not already on a slide with the same name
 
4308
                        var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
 
4309
 
 
4310
                        if( element ) {
 
4311
                                // If the slide exists and is not the current slide...
 
4312
                                if ( !isSameNameAsCurrentSlide ) {
 
4313
                                        // ...find the position of the named slide and navigate to it
 
4314
                                        var indices = Reveal.getIndices(element);
 
4315
                                        slide(indices.h, indices.v);
 
4316
                                }
 
4317
                        }
 
4318
                        // If the slide doesn't exist, navigate to the current slide
 
4319
                        else {
 
4320
                                slide( indexh || 0, indexv || 0 );
 
4321
                        }
 
4322
                }
 
4323
                else {
 
4324
                        var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
 
4325
 
 
4326
                        // Read the index components of the hash
 
4327
                        var h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
 
4328
                                v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
 
4329
                                f;
 
4330
 
 
4331
                        if( config.fragmentInURL ) {
 
4332
                                f = parseInt( bits[2], 10 );
 
4333
                                if( isNaN( f ) ) {
 
4334
                                        f = undefined;
 
4335
                                }
 
4336
                        }
 
4337
 
 
4338
                        if( h !== indexh || v !== indexv || f !== undefined ) {
 
4339
                                slide( h, v, f );
 
4340
                        }
 
4341
                }
 
4342
 
 
4343
        }
 
4344
 
 
4345
        /**
 
4346
         * Updates the page URL (hash) to reflect the current
 
4347
         * state.
 
4348
         *
 
4349
         * @param {number} delay The time in ms to wait before
 
4350
         * writing the hash
 
4351
         */
 
4352
        function writeURL( delay ) {
 
4353
 
 
4354
                // Make sure there's never more than one timeout running
 
4355
                clearTimeout( writeURLTimeout );
 
4356
 
 
4357
                // If a delay is specified, timeout this call
 
4358
                if( typeof delay === 'number' ) {
 
4359
                        writeURLTimeout = setTimeout( writeURL, delay );
 
4360
                }
 
4361
                else if( currentSlide ) {
 
4362
                        // If we're configured to push to history OR the history
 
4363
                        // API is not avaialble.
 
4364
                        if( config.history || !window.history ) {
 
4365
                                window.location.hash = locationHash();
 
4366
                        }
 
4367
                        // If we're configured to reflect the current slide in the
 
4368
                        // URL without pushing to history.
 
4369
                        else if( config.hash ) {
 
4370
                                window.history.replaceState( null, null, '#' + locationHash() );
 
4371
                        }
 
4372
                        // If history and hash are both disabled, a hash may still
 
4373
                        // be added to the URL by clicking on a href with a hash
 
4374
                        // target. Counter this by always removing the hash.
 
4375
                        else {
 
4376
                                window.history.replaceState( null, null, window.location.pathname + window.location.search );
 
4377
                        }
 
4378
                }
 
4379
 
 
4380
        }
 
4381
        /**
 
4382
         * Retrieves the h/v location and fragment of the current,
 
4383
         * or specified, slide.
 
4384
         *
 
4385
         * @param {HTMLElement} [slide] If specified, the returned
 
4386
         * index will be for this slide rather than the currently
 
4387
         * active one
 
4388
         *
 
4389
         * @return {{h: number, v: number, f: number}}
 
4390
         */
 
4391
        function getIndices( slide ) {
 
4392
 
 
4393
                // By default, return the current indices
 
4394
                var h = indexh,
 
4395
                        v = indexv,
 
4396
                        f;
 
4397
 
 
4398
                // If a slide is specified, return the indices of that slide
 
4399
                if( slide ) {
 
4400
                        var isVertical = isVerticalSlide( slide );
 
4401
                        var slideh = isVertical ? slide.parentNode : slide;
 
4402
 
 
4403
                        // Select all horizontal slides
 
4404
                        var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
 
4405
 
 
4406
                        // Now that we know which the horizontal slide is, get its index
 
4407
                        h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
 
4408
 
 
4409
                        // Assume we're not vertical
 
4410
                        v = undefined;
 
4411
 
 
4412
                        // If this is a vertical slide, grab the vertical index
 
4413
                        if( isVertical ) {
 
4414
                                v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
 
4415
                        }
 
4416
                }
 
4417
 
 
4418
                if( !slide && currentSlide ) {
 
4419
                        var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
 
4420
                        if( hasFragments ) {
 
4421
                                var currentFragment = currentSlide.querySelector( '.current-fragment' );
 
4422
                                if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
 
4423
                                        f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
 
4424
                                }
 
4425
                                else {
 
4426
                                        f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
 
4427
                                }
 
4428
                        }
 
4429
                }
 
4430
 
 
4431
                return { h: h, v: v, f: f };
 
4432
 
 
4433
        }
 
4434
 
 
4435
        /**
 
4436
         * Retrieves all slides in this presentation.
 
4437
         */
 
4438
        function getSlides() {
 
4439
 
 
4440
                return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
 
4441
 
 
4442
        }
 
4443
 
 
4444
        /**
 
4445
         * Returns an array of objects where each object represents the
 
4446
         * attributes on its respective slide.
 
4447
         */
 
4448
        function getSlidesAttributes() {
 
4449
 
 
4450
                return getSlides().map( function( slide ) {
 
4451
 
 
4452
                        var attributes = {};
 
4453
                        for( var i = 0; i < slide.attributes.length; i++ ) {
 
4454
                                var attribute = slide.attributes[ i ];
 
4455
                                attributes[ attribute.name ] = attribute.value;
 
4456
                        }
 
4457
                        return attributes;
 
4458
 
 
4459
                } );
 
4460
 
 
4461
        }
 
4462
 
 
4463
        /**
 
4464
         * Retrieves the total number of slides in this presentation.
 
4465
         *
 
4466
         * @return {number}
 
4467
         */
 
4468
        function getTotalSlides() {
 
4469
 
 
4470
                return getSlides().length;
 
4471
 
 
4472
        }
 
4473
 
 
4474
        /**
 
4475
         * Returns the slide element matching the specified index.
 
4476
         *
 
4477
         * @return {HTMLElement}
 
4478
         */
 
4479
        function getSlide( x, y ) {
 
4480
 
 
4481
                var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
 
4482
                var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
 
4483
 
 
4484
                if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
 
4485
                        return verticalSlides ? verticalSlides[ y ] : undefined;
 
4486
                }
 
4487
 
 
4488
                return horizontalSlide;
 
4489
 
 
4490
        }
 
4491
 
 
4492
        /**
 
4493
         * Returns the background element for the given slide.
 
4494
         * All slides, even the ones with no background properties
 
4495
         * defined, have a background element so as long as the
 
4496
         * index is valid an element will be returned.
 
4497
         *
 
4498
         * @param {mixed} x Horizontal background index OR a slide
 
4499
         * HTML element
 
4500
         * @param {number} y Vertical background index
 
4501
         * @return {(HTMLElement[]|*)}
 
4502
         */
 
4503
        function getSlideBackground( x, y ) {
 
4504
 
 
4505
                var slide = typeof x === 'number' ? getSlide( x, y ) : x;
 
4506
                if( slide ) {
 
4507
                        return slide.slideBackgroundElement;
 
4508
                }
 
4509
 
 
4510
                return undefined;
 
4511
 
 
4512
        }
 
4513
 
 
4514
        /**
 
4515
         * Retrieves the speaker notes from a slide. Notes can be
 
4516
         * defined in two ways:
 
4517
         * 1. As a data-notes attribute on the slide <section>
 
4518
         * 2. As an <aside class="notes"> inside of the slide
 
4519
         *
 
4520
         * @param {HTMLElement} [slide=currentSlide]
 
4521
         * @return {(string|null)}
 
4522
         */
 
4523
        function getSlideNotes( slide ) {
 
4524
 
 
4525
                // Default to the current slide
 
4526
                slide = slide || currentSlide;
 
4527
 
 
4528
                // Notes can be specified via the data-notes attribute...
 
4529
                if( slide.hasAttribute( 'data-notes' ) ) {
 
4530
                        return slide.getAttribute( 'data-notes' );
 
4531
                }
 
4532
 
 
4533
                // ... or using an <aside class="notes"> element
 
4534
                var notesElement = slide.querySelector( 'aside.notes' );
 
4535
                if( notesElement ) {
 
4536
                        return notesElement.innerHTML;
 
4537
                }
 
4538
 
 
4539
                return null;
 
4540
 
 
4541
        }
 
4542
 
 
4543
        /**
 
4544
         * Retrieves the current state of the presentation as
 
4545
         * an object. This state can then be restored at any
 
4546
         * time.
 
4547
         *
 
4548
         * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
 
4549
         */
 
4550
        function getState() {
 
4551
 
 
4552
                var indices = getIndices();
 
4553
 
 
4554
                return {
 
4555
                        indexh: indices.h,
 
4556
                        indexv: indices.v,
 
4557
                        indexf: indices.f,
 
4558
                        paused: isPaused(),
 
4559
                        overview: isOverview()
 
4560
                };
 
4561
 
 
4562
        }
 
4563
 
 
4564
        /**
 
4565
         * Restores the presentation to the given state.
 
4566
         *
 
4567
         * @param {object} state As generated by getState()
 
4568
         * @see {@link getState} generates the parameter `state`
 
4569
         */
 
4570
        function setState( state ) {
 
4571
 
 
4572
                if( typeof state === 'object' ) {
 
4573
                        slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
 
4574
 
 
4575
                        var pausedFlag = deserialize( state.paused ),
 
4576
                                overviewFlag = deserialize( state.overview );
 
4577
 
 
4578
                        if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
 
4579
                                togglePause( pausedFlag );
 
4580
                        }
 
4581
 
 
4582
                        if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
 
4583
                                toggleOverview( overviewFlag );
 
4584
                        }
 
4585
                }
 
4586
 
 
4587
        }
 
4588
 
 
4589
        /**
 
4590
         * Return a sorted fragments list, ordered by an increasing
 
4591
         * "data-fragment-index" attribute.
 
4592
         *
 
4593
         * Fragments will be revealed in the order that they are returned by
 
4594
         * this function, so you can use the index attributes to control the
 
4595
         * order of fragment appearance.
 
4596
         *
 
4597
         * To maintain a sensible default fragment order, fragments are presumed
 
4598
         * to be passed in document order. This function adds a "fragment-index"
 
4599
         * attribute to each node if such an attribute is not already present,
 
4600
         * and sets that attribute to an integer value which is the position of
 
4601
         * the fragment within the fragments list.
 
4602
         *
 
4603
         * @param {object[]|*} fragments
 
4604
         * @param {boolean} grouped If true the returned array will contain
 
4605
         * nested arrays for all fragments with the same index
 
4606
         * @return {object[]} sorted Sorted array of fragments
 
4607
         */
 
4608
        function sortFragments( fragments, grouped ) {
 
4609
 
 
4610
                fragments = toArray( fragments );
 
4611
 
 
4612
                var ordered = [],
 
4613
                        unordered = [],
 
4614
                        sorted = [];
 
4615
 
 
4616
                // Group ordered and unordered elements
 
4617
                fragments.forEach( function( fragment, i ) {
 
4618
                        if( fragment.hasAttribute( 'data-fragment-index' ) ) {
 
4619
                                var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
 
4620
 
 
4621
                                if( !ordered[index] ) {
 
4622
                                        ordered[index] = [];
 
4623
                                }
 
4624
 
 
4625
                                ordered[index].push( fragment );
 
4626
                        }
 
4627
                        else {
 
4628
                                unordered.push( [ fragment ] );
 
4629
                        }
 
4630
                } );
 
4631
 
 
4632
                // Append fragments without explicit indices in their
 
4633
                // DOM order
 
4634
                ordered = ordered.concat( unordered );
 
4635
 
 
4636
                // Manually count the index up per group to ensure there
 
4637
                // are no gaps
 
4638
                var index = 0;
 
4639
 
 
4640
                // Push all fragments in their sorted order to an array,
 
4641
                // this flattens the groups
 
4642
                ordered.forEach( function( group ) {
 
4643
                        group.forEach( function( fragment ) {
 
4644
                                sorted.push( fragment );
 
4645
                                fragment.setAttribute( 'data-fragment-index', index );
 
4646
                        } );
 
4647
 
 
4648
                        index ++;
 
4649
                } );
 
4650
 
 
4651
                return grouped === true ? ordered : sorted;
 
4652
 
 
4653
        }
 
4654
 
 
4655
        /**
 
4656
         * Refreshes the fragments on the current slide so that they
 
4657
         * have the appropriate classes (.visible + .current-fragment).
 
4658
         *
 
4659
         * @param {number} [index] The index of the current fragment
 
4660
         * @param {array} [fragments] Array containing all fragments
 
4661
         * in the current slide
 
4662
         *
 
4663
         * @return {{shown: array, hidden: array}}
 
4664
         */
 
4665
        function updateFragments( index, fragments ) {
 
4666
 
 
4667
                var changedFragments = {
 
4668
                        shown: [],
 
4669
                        hidden: []
 
4670
                };
 
4671
 
 
4672
                if( currentSlide && config.fragments ) {
 
4673
 
 
4674
                        fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
 
4675
 
 
4676
                        if( fragments.length ) {
 
4677
 
 
4678
                                if( typeof index !== 'number' ) {
 
4679
                                        var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
 
4680
                                        if( currentFragment ) {
 
4681
                                                index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
 
4682
                                        }
 
4683
                                }
 
4684
 
 
4685
                                toArray( fragments ).forEach( function( el, i ) {
 
4686
 
 
4687
                                        if( el.hasAttribute( 'data-fragment-index' ) ) {
 
4688
                                                i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
 
4689
                                        }
 
4690
 
 
4691
                                        // Visible fragments
 
4692
                                        if( i <= index ) {
 
4693
                                                if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
 
4694
                                                el.classList.add( 'visible' );
 
4695
                                                el.classList.remove( 'current-fragment' );
 
4696
 
 
4697
                                                // Announce the fragments one by one to the Screen Reader
 
4698
                                                dom.statusDiv.textContent = getStatusText( el );
 
4699
 
 
4700
                                                if( i === index ) {
 
4701
                                                        el.classList.add( 'current-fragment' );
 
4702
                                                        startEmbeddedContent( el );
 
4703
                                                }
 
4704
                                        }
 
4705
                                        // Hidden fragments
 
4706
                                        else {
 
4707
                                                if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
 
4708
                                                el.classList.remove( 'visible' );
 
4709
                                                el.classList.remove( 'current-fragment' );
 
4710
                                        }
 
4711
 
 
4712
                                } );
 
4713
 
 
4714
                        }
 
4715
 
 
4716
                }
 
4717
 
 
4718
                return changedFragments;
 
4719
 
 
4720
        }
 
4721
 
 
4722
        /**
 
4723
         * Navigate to the specified slide fragment.
 
4724
         *
 
4725
         * @param {?number} index The index of the fragment that
 
4726
         * should be shown, -1 means all are invisible
 
4727
         * @param {number} offset Integer offset to apply to the
 
4728
         * fragment index
 
4729
         *
 
4730
         * @return {boolean} true if a change was made in any
 
4731
         * fragments visibility as part of this call
 
4732
         */
 
4733
        function navigateFragment( index, offset ) {
 
4734
 
 
4735
                if( currentSlide && config.fragments ) {
 
4736
 
 
4737
                        var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
 
4738
                        if( fragments.length ) {
 
4739
 
 
4740
                                // If no index is specified, find the current
 
4741
                                if( typeof index !== 'number' ) {
 
4742
                                        var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
 
4743
 
 
4744
                                        if( lastVisibleFragment ) {
 
4745
                                                index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
 
4746
                                        }
 
4747
                                        else {
 
4748
                                                index = -1;
 
4749
                                        }
 
4750
                                }
 
4751
 
 
4752
                                // If an offset is specified, apply it to the index
 
4753
                                if( typeof offset === 'number' ) {
 
4754
                                        index += offset;
 
4755
                                }
 
4756
 
 
4757
                                var changedFragments = updateFragments( index, fragments );
 
4758
 
 
4759
                                if( changedFragments.hidden.length ) {
 
4760
                                        dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
 
4761
                                }
 
4762
 
 
4763
                                if( changedFragments.shown.length ) {
 
4764
                                        dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
 
4765
                                }
 
4766
 
 
4767
                                updateControls();
 
4768
                                updateProgress();
 
4769
 
 
4770
                                if( config.fragmentInURL ) {
 
4771
                                        writeURL();
 
4772
                                }
 
4773
 
 
4774
                                return !!( changedFragments.shown.length || changedFragments.hidden.length );
 
4775
 
 
4776
                        }
 
4777
 
 
4778
                }
 
4779
 
 
4780
                return false;
 
4781
 
 
4782
        }
 
4783
 
 
4784
        /**
 
4785
         * Navigate to the next slide fragment.
 
4786
         *
 
4787
         * @return {boolean} true if there was a next fragment,
 
4788
         * false otherwise
 
4789
         */
 
4790
        function nextFragment() {
 
4791
 
 
4792
                return navigateFragment( null, 1 );
 
4793
 
 
4794
        }
 
4795
 
 
4796
        /**
 
4797
         * Navigate to the previous slide fragment.
 
4798
         *
 
4799
         * @return {boolean} true if there was a previous fragment,
 
4800
         * false otherwise
 
4801
         */
 
4802
        function previousFragment() {
 
4803
 
 
4804
                return navigateFragment( null, -1 );
 
4805
 
 
4806
        }
 
4807
 
 
4808
        /**
 
4809
         * Cues a new automated slide if enabled in the config.
 
4810
         */
 
4811
        function cueAutoSlide() {
 
4812
 
 
4813
                cancelAutoSlide();
 
4814
 
 
4815
                if( currentSlide && config.autoSlide !== false ) {
 
4816
 
 
4817
                        var fragment = currentSlide.querySelector( '.current-fragment' );
 
4818
 
 
4819
                        // When the slide first appears there is no "current" fragment so
 
4820
                        // we look for a data-autoslide timing on the first fragment
 
4821
                        if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
 
4822
 
 
4823
                        var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
 
4824
                        var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
 
4825
                        var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
 
4826
 
 
4827
                        // Pick value in the following priority order:
 
4828
                        // 1. Current fragment's data-autoslide
 
4829
                        // 2. Current slide's data-autoslide
 
4830
                        // 3. Parent slide's data-autoslide
 
4831
                        // 4. Global autoSlide setting
 
4832
                        if( fragmentAutoSlide ) {
 
4833
                                autoSlide = parseInt( fragmentAutoSlide, 10 );
 
4834
                        }
 
4835
                        else if( slideAutoSlide ) {
 
4836
                                autoSlide = parseInt( slideAutoSlide, 10 );
 
4837
                        }
 
4838
                        else if( parentAutoSlide ) {
 
4839
                                autoSlide = parseInt( parentAutoSlide, 10 );
 
4840
                        }
 
4841
                        else {
 
4842
                                autoSlide = config.autoSlide;
 
4843
                        }
 
4844
 
 
4845
                        // If there are media elements with data-autoplay,
 
4846
                        // automatically set the autoSlide duration to the
 
4847
                        // length of that media. Not applicable if the slide
 
4848
                        // is divided up into fragments.
 
4849
                        // playbackRate is accounted for in the duration.
 
4850
                        if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
 
4851
                                toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
 
4852
                                        if( el.hasAttribute( 'data-autoplay' ) ) {
 
4853
                                                if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
 
4854
                                                        autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
 
4855
                                                }
 
4856
                                        }
 
4857
                                } );
 
4858
                        }
 
4859
 
 
4860
                        // Cue the next auto-slide if:
 
4861
                        // - There is an autoSlide value
 
4862
                        // - Auto-sliding isn't paused by the user
 
4863
                        // - The presentation isn't paused
 
4864
                        // - The overview isn't active
 
4865
                        // - The presentation isn't over
 
4866
                        if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
 
4867
                                autoSlideTimeout = setTimeout( function() {
 
4868
                                        typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
 
4869
                                        cueAutoSlide();
 
4870
                                }, autoSlide );
 
4871
                                autoSlideStartTime = Date.now();
 
4872
                        }
 
4873
 
 
4874
                        if( autoSlidePlayer ) {
 
4875
                                autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
 
4876
                        }
 
4877
 
 
4878
                }
 
4879
 
 
4880
        }
 
4881
 
 
4882
        /**
 
4883
         * Cancels any ongoing request to auto-slide.
 
4884
         */
 
4885
        function cancelAutoSlide() {
 
4886
 
 
4887
                clearTimeout( autoSlideTimeout );
 
4888
                autoSlideTimeout = -1;
 
4889
 
 
4890
        }
 
4891
 
 
4892
        function pauseAutoSlide() {
 
4893
 
 
4894
                if( autoSlide && !autoSlidePaused ) {
 
4895
                        autoSlidePaused = true;
 
4896
                        dispatchEvent( 'autoslidepaused' );
 
4897
                        clearTimeout( autoSlideTimeout );
 
4898
 
 
4899
                        if( autoSlidePlayer ) {
 
4900
                                autoSlidePlayer.setPlaying( false );
 
4901
                        }
 
4902
                }
 
4903
 
 
4904
        }
 
4905
 
 
4906
        function resumeAutoSlide() {
 
4907
 
 
4908
                if( autoSlide && autoSlidePaused ) {
 
4909
                        autoSlidePaused = false;
 
4910
                        dispatchEvent( 'autoslideresumed' );
 
4911
                        cueAutoSlide();
 
4912
                }
 
4913
 
 
4914
        }
 
4915
 
 
4916
        function navigateLeft() {
 
4917
 
 
4918
                // Reverse for RTL
 
4919
                if( config.rtl ) {
 
4920
                        if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
 
4921
                                slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
 
4922
                        }
 
4923
                }
 
4924
                // Normal navigation
 
4925
                else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
 
4926
                        slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
 
4927
                }
 
4928
 
 
4929
        }
 
4930
 
 
4931
        function navigateRight() {
 
4932
 
 
4933
                hasNavigatedRight = true;
 
4934
 
 
4935
                // Reverse for RTL
 
4936
                if( config.rtl ) {
 
4937
                        if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
 
4938
                                slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
 
4939
                        }
 
4940
                }
 
4941
                // Normal navigation
 
4942
                else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
 
4943
                        slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
 
4944
                }
 
4945
 
 
4946
        }
 
4947
 
 
4948
        function navigateUp() {
 
4949
 
 
4950
                // Prioritize hiding fragments
 
4951
                if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
 
4952
                        slide( indexh, indexv - 1 );
 
4953
                }
 
4954
 
 
4955
        }
 
4956
 
 
4957
        function navigateDown() {
 
4958
 
 
4959
                hasNavigatedDown = true;
 
4960
 
 
4961
                // Prioritize revealing fragments
 
4962
                if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
 
4963
                        slide( indexh, indexv + 1 );
 
4964
                }
 
4965
 
 
4966
        }
 
4967
 
 
4968
        /**
 
4969
         * Navigates backwards, prioritized in the following order:
 
4970
         * 1) Previous fragment
 
4971
         * 2) Previous vertical slide
 
4972
         * 3) Previous horizontal slide
 
4973
         */
 
4974
        function navigatePrev() {
 
4975
 
 
4976
                // Prioritize revealing fragments
 
4977
                if( previousFragment() === false ) {
 
4978
                        if( availableRoutes().up ) {
 
4979
                                navigateUp();
 
4980
                        }
 
4981
                        else {
 
4982
                                // Fetch the previous horizontal slide, if there is one
 
4983
                                var previousSlide;
 
4984
 
 
4985
                                if( config.rtl ) {
 
4986
                                        previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
 
4987
                                }
 
4988
                                else {
 
4989
                                        previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
 
4990
                                }
 
4991
 
 
4992
                                if( previousSlide ) {
 
4993
                                        var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
 
4994
                                        var h = indexh - 1;
 
4995
                                        slide( h, v );
 
4996
                                }
 
4997
                        }
 
4998
                }
 
4999
 
 
5000
        }
 
5001
 
 
5002
        /**
 
5003
         * The reverse of #navigatePrev().
 
5004
         */
 
5005
        function navigateNext() {
 
5006
 
 
5007
                hasNavigatedRight = true;
 
5008
                hasNavigatedDown = true;
 
5009
 
 
5010
                // Prioritize revealing fragments
 
5011
                if( nextFragment() === false ) {
 
5012
 
 
5013
                        var routes = availableRoutes();
 
5014
 
 
5015
                        // When looping is enabled `routes.down` is always available
 
5016
                        // so we need a separate check for when we've reached the
 
5017
                        // end of a stack and should move horizontally
 
5018
                        if( routes.down && routes.right && config.loop && Reveal.isLastVerticalSlide( currentSlide ) ) {
 
5019
                                routes.down = false;
 
5020
                        }
 
5021
 
 
5022
                        if( routes.down ) {
 
5023
                                navigateDown();
 
5024
                        }
 
5025
                        else if( config.rtl ) {
 
5026
                                navigateLeft();
 
5027
                        }
 
5028
                        else {
 
5029
                                navigateRight();
 
5030
                        }
 
5031
                }
 
5032
 
 
5033
        }
 
5034
 
 
5035
        /**
 
5036
         * Checks if the target element prevents the triggering of
 
5037
         * swipe navigation.
 
5038
         */
 
5039
        function isSwipePrevented( target ) {
 
5040
 
 
5041
                while( target && typeof target.hasAttribute === 'function' ) {
 
5042
                        if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
 
5043
                        target = target.parentNode;
 
5044
                }
 
5045
 
 
5046
                return false;
 
5047
 
 
5048
        }
 
5049
 
 
5050
 
 
5051
        // --------------------------------------------------------------------//
 
5052
        // ----------------------------- EVENTS -------------------------------//
 
5053
        // --------------------------------------------------------------------//
 
5054
 
 
5055
        /**
 
5056
         * Called by all event handlers that are based on user
 
5057
         * input.
 
5058
         *
 
5059
         * @param {object} [event]
 
5060
         */
 
5061
        function onUserInput( event ) {
 
5062
 
 
5063
                if( config.autoSlideStoppable ) {
 
5064
                        pauseAutoSlide();
 
5065
                }
 
5066
 
 
5067
        }
 
5068
 
 
5069
        /**
 
5070
         * Called whenever there is mouse input at the document level
 
5071
         * to determine if the cursor is active or not.
 
5072
         *
 
5073
         * @param {object} event
 
5074
         */
 
5075
        function onDocumentCursorActive( event ) {
 
5076
 
 
5077
                showCursor();
 
5078
 
 
5079
                clearTimeout( cursorInactiveTimeout );
 
5080
 
 
5081
                cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
 
5082
 
 
5083
        }
 
5084
 
 
5085
        /**
 
5086
         * Handler for the document level 'keypress' event.
 
5087
         *
 
5088
         * @param {object} event
 
5089
         */
 
5090
        function onDocumentKeyPress( event ) {
 
5091
 
 
5092
                // Check if the pressed key is question mark
 
5093
                if( event.shiftKey && event.charCode === 63 ) {
 
5094
                        toggleHelp();
 
5095
                }
 
5096
 
 
5097
        }
 
5098
 
 
5099
        /**
 
5100
         * Handler for the document level 'keydown' event.
 
5101
         *
 
5102
         * @param {object} event
 
5103
         */
 
5104
        function onDocumentKeyDown( event ) {
 
5105
 
 
5106
                // If there's a condition specified and it returns false,
 
5107
                // ignore this event
 
5108
                if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
 
5109
                        return true;
 
5110
                }
 
5111
 
 
5112
                // Shorthand
 
5113
                var keyCode = event.keyCode;
 
5114
 
 
5115
                // Remember if auto-sliding was paused so we can toggle it
 
5116
                var autoSlideWasPaused = autoSlidePaused;
 
5117
 
 
5118
                onUserInput( event );
 
5119
 
 
5120
                // Is there a focused element that could be using the keyboard?
 
5121
                var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
 
5122
                var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
 
5123
                var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
 
5124
 
 
5125
                // Whitelist specific modified + keycode combinations
 
5126
                var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
 
5127
                var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
 
5128
                var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
 
5129
 
 
5130
                // Prevent all other events when a modifier is pressed
 
5131
                var unusedModifier =    !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
 
5132
                                                                ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
 
5133
 
 
5134
                // Disregard the event if there's a focused element or a
 
5135
                // keyboard modifier key is present
 
5136
                if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
 
5137
 
 
5138
                // While paused only allow resume keyboard events; 'b', 'v', '.'
 
5139
                var resumeKeyCodes = [66,86,190,191];
 
5140
                var key;
 
5141
 
 
5142
                // Custom key bindings for togglePause should be able to resume
 
5143
                if( typeof config.keyboard === 'object' ) {
 
5144
                        for( key in config.keyboard ) {
 
5145
                                if( config.keyboard[key] === 'togglePause' ) {
 
5146
                                        resumeKeyCodes.push( parseInt( key, 10 ) );
 
5147
                                }
 
5148
                        }
 
5149
                }
 
5150
 
 
5151
                if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
 
5152
                        return false;
 
5153
                }
 
5154
 
 
5155
                var triggered = false;
 
5156
 
 
5157
                // 1. User defined key bindings
 
5158
                if( typeof config.keyboard === 'object' ) {
 
5159
 
 
5160
                        for( key in config.keyboard ) {
 
5161
 
 
5162
                                // Check if this binding matches the pressed key
 
5163
                                if( parseInt( key, 10 ) === keyCode ) {
 
5164
 
 
5165
                                        var value = config.keyboard[ key ];
 
5166
 
 
5167
                                        // Callback function
 
5168
                                        if( typeof value === 'function' ) {
 
5169
                                                value.apply( null, [ event ] );
 
5170
                                        }
 
5171
                                        // String shortcuts to reveal.js API
 
5172
                                        else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
 
5173
                                                Reveal[ value ].call();
 
5174
                                        }
 
5175
 
 
5176
                                        triggered = true;
 
5177
 
 
5178
                                }
 
5179
 
 
5180
                        }
 
5181
 
 
5182
                }
 
5183
 
 
5184
                // 2. Registered custom key bindings
 
5185
                if( triggered === false ) {
 
5186
 
 
5187
                        for( key in registeredKeyBindings ) {
 
5188
 
 
5189
                                // Check if this binding matches the pressed key
 
5190
                                if( parseInt( key, 10 ) === keyCode ) {
 
5191
 
 
5192
                                        var action = registeredKeyBindings[ key ].callback;
 
5193
 
 
5194
                                        // Callback function
 
5195
                                        if( typeof action === 'function' ) {
 
5196
                                                action.apply( null, [ event ] );
 
5197
                                        }
 
5198
                                        // String shortcuts to reveal.js API
 
5199
                                        else if( typeof action === 'string' && typeof Reveal[ action ] === 'function' ) {
 
5200
                                                Reveal[ action ].call();
 
5201
                                        }
 
5202
 
 
5203
                                        triggered = true;
 
5204
                                }
 
5205
                        }
 
5206
                }
 
5207
 
 
5208
                // 3. System defined key bindings
 
5209
                if( triggered === false ) {
 
5210
 
 
5211
                        // Assume true and try to prove false
 
5212
                        triggered = true;
 
5213
 
 
5214
                        // P, PAGE UP
 
5215
                        if( keyCode === 80 || keyCode === 33 ) {
 
5216
                                navigatePrev();
 
5217
                        }
 
5218
                        // N, PAGE DOWN
 
5219
                        else if( keyCode === 78 || keyCode === 34 ) {
 
5220
                                navigateNext();
 
5221
                        }
 
5222
                        // H, LEFT
 
5223
                        else if( keyCode === 72 || keyCode === 37 ) {
 
5224
                                if( firstSlideShortcut ) {
 
5225
                                        slide( 0 );
 
5226
                                }
 
5227
                                else if( !isOverview() && config.navigationMode === 'linear' ) {
 
5228
                                        navigatePrev();
 
5229
                                }
 
5230
                                else {
 
5231
                                        navigateLeft();
 
5232
                                }
 
5233
                        }
 
5234
                        // L, RIGHT
 
5235
                        else if( keyCode === 76 || keyCode === 39 ) {
 
5236
                                if( lastSlideShortcut ) {
 
5237
                                        slide( Number.MAX_VALUE );
 
5238
                                }
 
5239
                                else if( !isOverview() && config.navigationMode === 'linear' ) {
 
5240
                                        navigateNext();
 
5241
                                }
 
5242
                                else {
 
5243
                                        navigateRight();
 
5244
                                }
 
5245
                        }
 
5246
                        // K, UP
 
5247
                        else if( keyCode === 75 || keyCode === 38 ) {
 
5248
                                if( !isOverview() && config.navigationMode === 'linear' ) {
 
5249
                                        navigatePrev();
 
5250
                                }
 
5251
                                else {
 
5252
                                        navigateUp();
 
5253
                                }
 
5254
                        }
 
5255
                        // J, DOWN
 
5256
                        else if( keyCode === 74 || keyCode === 40 ) {
 
5257
                                if( !isOverview() && config.navigationMode === 'linear' ) {
 
5258
                                        navigateNext();
 
5259
                                }
 
5260
                                else {
 
5261
                                        navigateDown();
 
5262
                                }
 
5263
                        }
 
5264
                        // HOME
 
5265
                        else if( keyCode === 36 ) {
 
5266
                                slide( 0 );
 
5267
                        }
 
5268
                        // END
 
5269
                        else if( keyCode === 35 ) {
 
5270
                                slide( Number.MAX_VALUE );
 
5271
                        }
 
5272
                        // SPACE
 
5273
                        else if( keyCode === 32 ) {
 
5274
                                if( isOverview() ) {
 
5275
                                        deactivateOverview();
 
5276
                                }
 
5277
                                if( event.shiftKey ) {
 
5278
                                        navigatePrev();
 
5279
                                }
 
5280
                                else {
 
5281
                                        navigateNext();
 
5282
                                }
 
5283
                        }
 
5284
                        // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
 
5285
                        else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
 
5286
                                togglePause();
 
5287
                        }
 
5288
                        // F
 
5289
                        else if( keyCode === 70 ) {
 
5290
                                enterFullscreen();
 
5291
                        }
 
5292
                        // A
 
5293
                        else if( keyCode === 65 ) {
 
5294
                                if ( config.autoSlideStoppable ) {
 
5295
                                        toggleAutoSlide( autoSlideWasPaused );
 
5296
                                }
 
5297
                        }
 
5298
                        else {
 
5299
                                triggered = false;
 
5300
                        }
 
5301
 
 
5302
                }
 
5303
 
 
5304
                // If the input resulted in a triggered action we should prevent
 
5305
                // the browsers default behavior
 
5306
                if( triggered ) {
 
5307
                        event.preventDefault && event.preventDefault();
 
5308
                }
 
5309
                // ESC or O key
 
5310
                else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
 
5311
                        if( dom.overlay ) {
 
5312
                                closeOverlay();
 
5313
                        }
 
5314
                        else {
 
5315
                                toggleOverview();
 
5316
                        }
 
5317
 
 
5318
                        event.preventDefault && event.preventDefault();
 
5319
                }
 
5320
 
 
5321
                // If auto-sliding is enabled we need to cue up
 
5322
                // another timeout
 
5323
                cueAutoSlide();
 
5324
 
 
5325
        }
 
5326
 
 
5327
        /**
 
5328
         * Handler for the 'touchstart' event, enables support for
 
5329
         * swipe and pinch gestures.
 
5330
         *
 
5331
         * @param {object} event
 
5332
         */
 
5333
        function onTouchStart( event ) {
 
5334
 
 
5335
                if( isSwipePrevented( event.target ) ) return true;
 
5336
 
 
5337
                touch.startX = event.touches[0].clientX;
 
5338
                touch.startY = event.touches[0].clientY;
 
5339
                touch.startCount = event.touches.length;
 
5340
 
 
5341
        }
 
5342
 
 
5343
        /**
 
5344
         * Handler for the 'touchmove' event.
 
5345
         *
 
5346
         * @param {object} event
 
5347
         */
 
5348
        function onTouchMove( event ) {
 
5349
 
 
5350
                if( isSwipePrevented( event.target ) ) return true;
 
5351
 
 
5352
                // Each touch should only trigger one action
 
5353
                if( !touch.captured ) {
 
5354
                        onUserInput( event );
 
5355
 
 
5356
                        var currentX = event.touches[0].clientX;
 
5357
                        var currentY = event.touches[0].clientY;
 
5358
 
 
5359
                        // There was only one touch point, look for a swipe
 
5360
                        if( event.touches.length === 1 && touch.startCount !== 2 ) {
 
5361
 
 
5362
                                var deltaX = currentX - touch.startX,
 
5363
                                        deltaY = currentY - touch.startY;
 
5364
 
 
5365
                                if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
 
5366
                                        touch.captured = true;
 
5367
                                        navigateLeft();
 
5368
                                }
 
5369
                                else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
 
5370
                                        touch.captured = true;
 
5371
                                        navigateRight();
 
5372
                                }
 
5373
                                else if( deltaY > touch.threshold ) {
 
5374
                                        touch.captured = true;
 
5375
                                        navigateUp();
 
5376
                                }
 
5377
                                else if( deltaY < -touch.threshold ) {
 
5378
                                        touch.captured = true;
 
5379
                                        navigateDown();
 
5380
                                }
 
5381
 
 
5382
                                // If we're embedded, only block touch events if they have
 
5383
                                // triggered an action
 
5384
                                if( config.embedded ) {
 
5385
                                        if( touch.captured || isVerticalSlide( currentSlide ) ) {
 
5386
                                                event.preventDefault();
 
5387
                                        }
 
5388
                                }
 
5389
                                // Not embedded? Block them all to avoid needless tossing
 
5390
                                // around of the viewport in iOS
 
5391
                                else {
 
5392
                                        event.preventDefault();
 
5393
                                }
 
5394
 
 
5395
                        }
 
5396
                }
 
5397
                // There's a bug with swiping on some Android devices unless
 
5398
                // the default action is always prevented
 
5399
                else if( UA.match( /android/gi ) ) {
 
5400
                        event.preventDefault();
 
5401
                }
 
5402
 
 
5403
        }
 
5404
 
 
5405
        /**
 
5406
         * Handler for the 'touchend' event.
 
5407
         *
 
5408
         * @param {object} event
 
5409
         */
 
5410
        function onTouchEnd( event ) {
 
5411
 
 
5412
                touch.captured = false;
 
5413
 
 
5414
        }
 
5415
 
 
5416
        /**
 
5417
         * Convert pointer down to touch start.
 
5418
         *
 
5419
         * @param {object} event
 
5420
         */
 
5421
        function onPointerDown( event ) {
 
5422
 
 
5423
                if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
 
5424
                        event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 
5425
                        onTouchStart( event );
 
5426
                }
 
5427
 
 
5428
        }
 
5429
 
 
5430
        /**
 
5431
         * Convert pointer move to touch move.
 
5432
         *
 
5433
         * @param {object} event
 
5434
         */
 
5435
        function onPointerMove( event ) {
 
5436
 
 
5437
                if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
 
5438
                        event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 
5439
                        onTouchMove( event );
 
5440
                }
 
5441
 
 
5442
        }
 
5443
 
 
5444
        /**
 
5445
         * Convert pointer up to touch end.
 
5446
         *
 
5447
         * @param {object} event
 
5448
         */
 
5449
        function onPointerUp( event ) {
 
5450
 
 
5451
                if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
 
5452
                        event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 
5453
                        onTouchEnd( event );
 
5454
                }
 
5455
 
 
5456
        }
 
5457
 
 
5458
        /**
 
5459
         * Handles mouse wheel scrolling, throttled to avoid skipping
 
5460
         * multiple slides.
 
5461
         *
 
5462
         * @param {object} event
 
5463
         */
 
5464
        function onDocumentMouseScroll( event ) {
 
5465
 
 
5466
                if( Date.now() - lastMouseWheelStep > 600 ) {
 
5467
 
 
5468
                        lastMouseWheelStep = Date.now();
 
5469
 
 
5470
                        var delta = event.detail || -event.wheelDelta;
 
5471
                        if( delta > 0 ) {
 
5472
                                navigateNext();
 
5473
                        }
 
5474
                        else if( delta < 0 ) {
 
5475
                                navigatePrev();
 
5476
                        }
 
5477
 
 
5478
                }
 
5479
 
 
5480
        }
 
5481
 
 
5482
        /**
 
5483
         * Clicking on the progress bar results in a navigation to the
 
5484
         * closest approximate horizontal slide using this equation:
 
5485
         *
 
5486
         * ( clickX / presentationWidth ) * numberOfSlides
 
5487
         *
 
5488
         * @param {object} event
 
5489
         */
 
5490
        function onProgressClicked( event ) {
 
5491
 
 
5492
                onUserInput( event );
 
5493
 
 
5494
                event.preventDefault();
 
5495
 
 
5496
                var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
 
5497
                var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
 
5498
 
 
5499
                if( config.rtl ) {
 
5500
                        slideIndex = slidesTotal - slideIndex;
 
5501
                }
 
5502
 
 
5503
                slide( slideIndex );
 
5504
 
 
5505
        }
 
5506
 
 
5507
        /**
 
5508
         * Event handler for navigation control buttons.
 
5509
         */
 
5510
        function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
 
5511
        function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
 
5512
        function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
 
5513
        function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
 
5514
        function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
 
5515
        function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
 
5516
 
 
5517
        /**
 
5518
         * Handler for the window level 'hashchange' event.
 
5519
         *
 
5520
         * @param {object} [event]
 
5521
         */
 
5522
        function onWindowHashChange( event ) {
 
5523
 
 
5524
                readURL();
 
5525
 
 
5526
        }
 
5527
 
 
5528
        /**
 
5529
         * Handler for the window level 'resize' event.
 
5530
         *
 
5531
         * @param {object} [event]
 
5532
         */
 
5533
        function onWindowResize( event ) {
 
5534
 
 
5535
                layout();
 
5536
 
 
5537
        }
 
5538
 
 
5539
        /**
 
5540
         * Handle for the window level 'visibilitychange' event.
 
5541
         *
 
5542
         * @param {object} [event]
 
5543
         */
 
5544
        function onPageVisibilityChange( event ) {
 
5545
 
 
5546
                var isHidden =  document.webkitHidden ||
 
5547
                                                document.msHidden ||
 
5548
                                                document.hidden;
 
5549
 
 
5550
                // If, after clicking a link or similar and we're coming back,
 
5551
                // focus the document.body to ensure we can use keyboard shortcuts
 
5552
                if( isHidden === false && document.activeElement !== document.body ) {
 
5553
                        // Not all elements support .blur() - SVGs among them.
 
5554
                        if( typeof document.activeElement.blur === 'function' ) {
 
5555
                                document.activeElement.blur();
 
5556
                        }
 
5557
                        document.body.focus();
 
5558
                }
 
5559
 
 
5560
        }
 
5561
 
 
5562
        /**
 
5563
         * Invoked when a slide is and we're in the overview.
 
5564
         *
 
5565
         * @param {object} event
 
5566
         */
 
5567
        function onOverviewSlideClicked( event ) {
 
5568
 
 
5569
                // TODO There's a bug here where the event listeners are not
 
5570
                // removed after deactivating the overview.
 
5571
                if( eventsAreBound && isOverview() ) {
 
5572
                        event.preventDefault();
 
5573
 
 
5574
                        var element = event.target;
 
5575
 
 
5576
                        while( element && !element.nodeName.match( /section/gi ) ) {
 
5577
                                element = element.parentNode;
 
5578
                        }
 
5579
 
 
5580
                        if( element && !element.classList.contains( 'disabled' ) ) {
 
5581
 
 
5582
                                deactivateOverview();
 
5583
 
 
5584
                                if( element.nodeName.match( /section/gi ) ) {
 
5585
                                        var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
 
5586
                                                v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
 
5587
 
 
5588
                                        slide( h, v );
 
5589
                                }
 
5590
 
 
5591
                        }
 
5592
                }
 
5593
 
 
5594
        }
 
5595
 
 
5596
        /**
 
5597
         * Handles clicks on links that are set to preview in the
 
5598
         * iframe overlay.
 
5599
         *
 
5600
         * @param {object} event
 
5601
         */
 
5602
        function onPreviewLinkClicked( event ) {
 
5603
 
 
5604
                if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
 
5605
                        var url = event.currentTarget.getAttribute( 'href' );
 
5606
                        if( url ) {
 
5607
                                showPreview( url );
 
5608
                                event.preventDefault();
 
5609
                        }
 
5610
                }
 
5611
 
 
5612
        }
 
5613
 
 
5614
        /**
 
5615
         * Handles click on the auto-sliding controls element.
 
5616
         *
 
5617
         * @param {object} [event]
 
5618
         */
 
5619
        function onAutoSlidePlayerClick( event ) {
 
5620
 
 
5621
                // Replay
 
5622
                if( Reveal.isLastSlide() && config.loop === false ) {
 
5623
                        slide( 0, 0 );
 
5624
                        resumeAutoSlide();
 
5625
                }
 
5626
                // Resume
 
5627
                else if( autoSlidePaused ) {
 
5628
                        resumeAutoSlide();
 
5629
                }
 
5630
                // Pause
 
5631
                else {
 
5632
                        pauseAutoSlide();
 
5633
                }
 
5634
 
 
5635
        }
 
5636
 
 
5637
 
 
5638
        // --------------------------------------------------------------------//
 
5639
        // ------------------------ PLAYBACK COMPONENT ------------------------//
 
5640
        // --------------------------------------------------------------------//
 
5641
 
 
5642
 
 
5643
        /**
 
5644
         * Constructor for the playback component, which displays
 
5645
         * play/pause/progress controls.
 
5646
         *
 
5647
         * @param {HTMLElement} container The component will append
 
5648
         * itself to this
 
5649
         * @param {function} progressCheck A method which will be
 
5650
         * called frequently to get the current progress on a range
 
5651
         * of 0-1
 
5652
         */
 
5653
        function Playback( container, progressCheck ) {
 
5654
 
 
5655
                // Cosmetics
 
5656
                this.diameter = 100;
 
5657
                this.diameter2 = this.diameter/2;
 
5658
                this.thickness = 6;
 
5659
 
 
5660
                // Flags if we are currently playing
 
5661
                this.playing = false;
 
5662
 
 
5663
                // Current progress on a 0-1 range
 
5664
                this.progress = 0;
 
5665
 
 
5666
                // Used to loop the animation smoothly
 
5667
                this.progressOffset = 1;
 
5668
 
 
5669
                this.container = container;
 
5670
                this.progressCheck = progressCheck;
 
5671
 
 
5672
                this.canvas = document.createElement( 'canvas' );
 
5673
                this.canvas.className = 'playback';
 
5674
                this.canvas.width = this.diameter;
 
5675
                this.canvas.height = this.diameter;
 
5676
                this.canvas.style.width = this.diameter2 + 'px';
 
5677
                this.canvas.style.height = this.diameter2 + 'px';
 
5678
                this.context = this.canvas.getContext( '2d' );
 
5679
 
 
5680
                this.container.appendChild( this.canvas );
 
5681
 
 
5682
                this.render();
 
5683
 
 
5684
        }
 
5685
 
 
5686
        /**
 
5687
         * @param value
 
5688
         */
 
5689
        Playback.prototype.setPlaying = function( value ) {
 
5690
 
 
5691
                var wasPlaying = this.playing;
 
5692
 
 
5693
                this.playing = value;
 
5694
 
 
5695
                // Start repainting if we weren't already
 
5696
                if( !wasPlaying && this.playing ) {
 
5697
                        this.animate();
 
5698
                }
 
5699
                else {
 
5700
                        this.render();
 
5701
                }
 
5702
 
 
5703
        };
 
5704
 
 
5705
        Playback.prototype.animate = function() {
 
5706
 
 
5707
                var progressBefore = this.progress;
 
5708
 
 
5709
                this.progress = this.progressCheck();
 
5710
 
 
5711
                // When we loop, offset the progress so that it eases
 
5712
                // smoothly rather than immediately resetting
 
5713
                if( progressBefore > 0.8 && this.progress < 0.2 ) {
 
5714
                        this.progressOffset = this.progress;
 
5715
                }
 
5716
 
 
5717
                this.render();
 
5718
 
 
5719
                if( this.playing ) {
 
5720
                        features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
 
5721
                }
 
5722
 
 
5723
        };
 
5724
 
 
5725
        /**
 
5726
         * Renders the current progress and playback state.
 
5727
         */
 
5728
        Playback.prototype.render = function() {
 
5729
 
 
5730
                var progress = this.playing ? this.progress : 0,
 
5731
                        radius = ( this.diameter2 ) - this.thickness,
 
5732
                        x = this.diameter2,
 
5733
                        y = this.diameter2,
 
5734
                        iconSize = 28;
 
5735
 
 
5736
                // Ease towards 1
 
5737
                this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
 
5738
 
 
5739
                var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
 
5740
                var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
 
5741
 
 
5742
                this.context.save();
 
5743
                this.context.clearRect( 0, 0, this.diameter, this.diameter );
 
5744
 
 
5745
                // Solid background color
 
5746
                this.context.beginPath();
 
5747
                this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
 
5748
                this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
 
5749
                this.context.fill();
 
5750
 
 
5751
                // Draw progress track
 
5752
                this.context.beginPath();
 
5753
                this.context.arc( x, y, radius, 0, Math.PI * 2, false );
 
5754
                this.context.lineWidth = this.thickness;
 
5755
                this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
 
5756
                this.context.stroke();
 
5757
 
 
5758
                if( this.playing ) {
 
5759
                        // Draw progress on top of track
 
5760
                        this.context.beginPath();
 
5761
                        this.context.arc( x, y, radius, startAngle, endAngle, false );
 
5762
                        this.context.lineWidth = this.thickness;
 
5763
                        this.context.strokeStyle = '#fff';
 
5764
                        this.context.stroke();
 
5765
                }
 
5766
 
 
5767
                this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
 
5768
 
 
5769
                // Draw play/pause icons
 
5770
                if( this.playing ) {
 
5771
                        this.context.fillStyle = '#fff';
 
5772
                        this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
 
5773
                        this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
 
5774
                }
 
5775
                else {
 
5776
                        this.context.beginPath();
 
5777
                        this.context.translate( 4, 0 );
 
5778
                        this.context.moveTo( 0, 0 );
 
5779
                        this.context.lineTo( iconSize - 4, iconSize / 2 );
 
5780
                        this.context.lineTo( 0, iconSize );
 
5781
                        this.context.fillStyle = '#fff';
 
5782
                        this.context.fill();
 
5783
                }
 
5784
 
 
5785
                this.context.restore();
 
5786
 
 
5787
        };
 
5788
 
 
5789
        Playback.prototype.on = function( type, listener ) {
 
5790
                this.canvas.addEventListener( type, listener, false );
 
5791
        };
 
5792
 
 
5793
        Playback.prototype.off = function( type, listener ) {
 
5794
                this.canvas.removeEventListener( type, listener, false );
 
5795
        };
 
5796
 
 
5797
        Playback.prototype.destroy = function() {
 
5798
 
 
5799
                this.playing = false;
 
5800
 
 
5801
                if( this.canvas.parentNode ) {
 
5802
                        this.container.removeChild( this.canvas );
 
5803
                }
 
5804
 
 
5805
        };
 
5806
 
 
5807
 
 
5808
        // --------------------------------------------------------------------//
 
5809
        // ------------------------------- API --------------------------------//
 
5810
        // --------------------------------------------------------------------//
 
5811
 
 
5812
 
 
5813
        Reveal = {
 
5814
                VERSION: VERSION,
 
5815
 
 
5816
                initialize: initialize,
 
5817
                reinitialize: reinitialize,
 
5818
                configure: configure,
 
5819
 
 
5820
                sync: sync,
 
5821
                syncSlide: syncSlide,
 
5822
                syncFragments: syncFragments,
 
5823
 
 
5824
                // Navigation methods
 
5825
                slide: slide,
 
5826
                left: navigateLeft,
 
5827
                right: navigateRight,
 
5828
                up: navigateUp,
 
5829
                down: navigateDown,
 
5830
                prev: navigatePrev,
 
5831
                next: navigateNext,
 
5832
 
 
5833
                // Fragment methods
 
5834
                navigateFragment: navigateFragment,
 
5835
                prevFragment: previousFragment,
 
5836
                nextFragment: nextFragment,
 
5837
 
 
5838
                // Deprecated aliases
 
5839
                navigateTo: slide,
 
5840
                navigateLeft: navigateLeft,
 
5841
                navigateRight: navigateRight,
 
5842
                navigateUp: navigateUp,
 
5843
                navigateDown: navigateDown,
 
5844
                navigatePrev: navigatePrev,
 
5845
                navigateNext: navigateNext,
 
5846
 
 
5847
                // Forces an update in slide layout
 
5848
                layout: layout,
 
5849
 
 
5850
                // Randomizes the order of slides
 
5851
                shuffle: shuffle,
 
5852
 
 
5853
                // Returns an object with the available routes as booleans (left/right/top/bottom)
 
5854
                availableRoutes: availableRoutes,
 
5855
 
 
5856
                // Returns an object with the available fragments as booleans (prev/next)
 
5857
                availableFragments: availableFragments,
 
5858
 
 
5859
                // Toggles a help overlay with keyboard shortcuts
 
5860
                toggleHelp: toggleHelp,
 
5861
 
 
5862
                // Toggles the overview mode on/off
 
5863
                toggleOverview: toggleOverview,
 
5864
 
 
5865
                // Toggles the "black screen" mode on/off
 
5866
                togglePause: togglePause,
 
5867
 
 
5868
                // Toggles the auto slide mode on/off
 
5869
                toggleAutoSlide: toggleAutoSlide,
 
5870
 
 
5871
                // State checks
 
5872
                isOverview: isOverview,
 
5873
                isPaused: isPaused,
 
5874
                isAutoSliding: isAutoSliding,
 
5875
                isSpeakerNotes: isSpeakerNotes,
 
5876
 
 
5877
                // Slide preloading
 
5878
                loadSlide: loadSlide,
 
5879
                unloadSlide: unloadSlide,
 
5880
 
 
5881
                // Adds or removes all internal event listeners (such as keyboard)
 
5882
                addEventListeners: addEventListeners,
 
5883
                removeEventListeners: removeEventListeners,
 
5884
 
 
5885
                // Facility for persisting and restoring the presentation state
 
5886
                getState: getState,
 
5887
                setState: setState,
 
5888
 
 
5889
                // Presentation progress
 
5890
                getSlidePastCount: getSlidePastCount,
 
5891
 
 
5892
                // Presentation progress on range of 0-1
 
5893
                getProgress: getProgress,
 
5894
 
 
5895
                // Returns the indices of the current, or specified, slide
 
5896
                getIndices: getIndices,
 
5897
 
 
5898
                // Returns an Array of all slides
 
5899
                getSlides: getSlides,
 
5900
 
 
5901
                // Returns an Array of objects representing the attributes on
 
5902
                // the slides
 
5903
                getSlidesAttributes: getSlidesAttributes,
 
5904
 
 
5905
                // Returns the total number of slides
 
5906
                getTotalSlides: getTotalSlides,
 
5907
 
 
5908
                // Returns the slide element at the specified index
 
5909
                getSlide: getSlide,
 
5910
 
 
5911
                // Returns the slide background element at the specified index
 
5912
                getSlideBackground: getSlideBackground,
 
5913
 
 
5914
                // Returns the speaker notes string for a slide, or null
 
5915
                getSlideNotes: getSlideNotes,
 
5916
 
 
5917
                // Returns the previous slide element, may be null
 
5918
                getPreviousSlide: function() {
 
5919
                        return previousSlide;
 
5920
                },
 
5921
 
 
5922
                // Returns the current slide element
 
5923
                getCurrentSlide: function() {
 
5924
                        return currentSlide;
 
5925
                },
 
5926
 
 
5927
                // Returns the current scale of the presentation content
 
5928
                getScale: function() {
 
5929
                        return scale;
 
5930
                },
 
5931
 
 
5932
                // Returns the current configuration object
 
5933
                getConfig: function() {
 
5934
                        return config;
 
5935
                },
 
5936
 
 
5937
                // Helper method, retrieves query string as a key/value hash
 
5938
                getQueryHash: function() {
 
5939
                        var query = {};
 
5940
 
 
5941
                        location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
 
5942
                                query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
 
5943
                        } );
 
5944
 
 
5945
                        // Basic deserialization
 
5946
                        for( var i in query ) {
 
5947
                                var value = query[ i ];
 
5948
 
 
5949
                                query[ i ] = deserialize( unescape( value ) );
 
5950
                        }
 
5951
 
 
5952
                        return query;
 
5953
                },
 
5954
 
 
5955
                // Returns the top-level DOM element
 
5956
                getRevealElement: function() {
 
5957
                        return dom.wrapper || document.querySelector( '.reveal' );
 
5958
                },
 
5959
 
 
5960
                // Returns a hash with all registered plugins
 
5961
                getPlugins: function() {
 
5962
                        return plugins;
 
5963
                },
 
5964
 
 
5965
                // Returns true if we're currently on the first slide
 
5966
                isFirstSlide: function() {
 
5967
                        return ( indexh === 0 && indexv === 0 );
 
5968
                },
 
5969
 
 
5970
                // Returns true if we're currently on the last slide
 
5971
                isLastSlide: function() {
 
5972
                        if( currentSlide ) {
 
5973
                                // Does this slide have a next sibling?
 
5974
                                if( currentSlide.nextElementSibling ) return false;
 
5975
 
 
5976
                                // If it's vertical, does its parent have a next sibling?
 
5977
                                if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
 
5978
 
 
5979
                                return true;
 
5980
                        }
 
5981
 
 
5982
                        return false;
 
5983
                },
 
5984
 
 
5985
                // Returns true if we're on the last slide in the current
 
5986
                // vertical stack
 
5987
                isLastVerticalSlide: function() {
 
5988
                        if( currentSlide && isVerticalSlide( currentSlide ) ) {
 
5989
                                // Does this slide have a next sibling?
 
5990
                                if( currentSlide.nextElementSibling ) return false;
 
5991
 
 
5992
                                return true;
 
5993
                        }
 
5994
 
 
5995
                        return false;
 
5996
                },
 
5997
 
 
5998
                // Checks if reveal.js has been loaded and is ready for use
 
5999
                isReady: function() {
 
6000
                        return loaded;
 
6001
                },
 
6002
 
 
6003
                // Forward event binding to the reveal DOM element
 
6004
                addEventListener: function( type, listener, useCapture ) {
 
6005
                        if( 'addEventListener' in window ) {
 
6006
                                Reveal.getRevealElement().addEventListener( type, listener, useCapture );
 
6007
                        }
 
6008
                },
 
6009
                removeEventListener: function( type, listener, useCapture ) {
 
6010
                        if( 'addEventListener' in window ) {
 
6011
                                Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
 
6012
                        }
 
6013
                },
 
6014
 
 
6015
                // Adds/removes a custom key binding
 
6016
                addKeyBinding: addKeyBinding,
 
6017
                removeKeyBinding: removeKeyBinding,
 
6018
 
 
6019
                // API for registering and retrieving plugins
 
6020
                registerPlugin: registerPlugin,
 
6021
                hasPlugin: hasPlugin,
 
6022
                getPlugin: getPlugin,
 
6023
 
 
6024
                // Programmatically triggers a keyboard event
 
6025
                triggerKey: function( keyCode ) {
 
6026
                        onDocumentKeyDown( { keyCode: keyCode } );
 
6027
                },
 
6028
 
 
6029
                // Registers a new shortcut to include in the help overlay
 
6030
                registerKeyboardShortcut: function( key, value ) {
 
6031
                        keyboardShortcuts[key] = value;
 
6032
                }
 
6033
        };
 
6034
 
 
6035
        return Reveal;
 
6036
 
 
6037
}));