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