6
* Copyright (C) 2019 Hakim El Hattab, http://hakim.se
8
(function( root, factory ) {
9
if( typeof define === 'function' && define.amd ) {
10
// AMD. Register as an anonymous module.
12
root.Reveal = factory();
15
} else if( typeof exports === 'object' ) {
16
// Node. Does not work with strict CommonJS.
17
module.exports = factory();
20
root.Reveal = factory();
28
// The reveal.js version
29
var VERSION = '3.8.0';
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,
37
// Configuration defaults, can be overridden at initialization time
40
// The "normal" size of the presentation, aspect ratio will be preserved
41
// when the presentation is scaled to fit different resolutions
45
// Factor of the display size that should remain empty around the content
48
// Bounds for smallest/largest possible scale to apply to content
52
// Display presentation control arrows
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,
59
// Determines where controls appear, "edges" or "bottom-right"
60
controlsLayout: 'bottom-right',
62
// Visibility rule for backwards navigation arrows; "faded", "hidden"
64
controlsBackArrows: 'faded',
66
// Display a presentation progress bar
69
// Display the page number of the current slide
70
// - true: Show slide number
71
// - false: Hide slide number
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
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().
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',
91
// Use 1 based indexing for # links to match slide number (default is zero
93
hashOneBasedIndex: false,
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
99
// Push each slide change to the browser history. Implies `hash: true`
102
// Enable keyboard shortcuts for navigation
105
// Optional function that blocks keyboard events when retuning false
106
keyboardCondition: null,
108
// Enable the slide overview mode
111
// Disables the default reveal.js slide layout so that you can use
113
disableLayout: false,
115
// Vertical centering of slides
118
// Enables touch navigation on devices with touch input
121
// Loop the presentation
124
// Change the presentation direction to be RTL
127
// Changes the behavior of our navigation directions.
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).
135
// Removes the up/down arrows. Left/right arrows step through all
136
// slides (both horizontal and vertical).
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
143
// Consider a deck with six slides ordered in two vertical stacks:
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
151
navigationMode: 'default',
153
// Randomizes the order of slides each time the presentation loads
156
// Turns fragments on and off globally
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,
163
// Flags if the presentation is running in an embedded mode,
164
// i.e. contained within a limited portion of the screen
167
// Flags if we should show a help overlay when the question-mark
171
// Flags if it should be possible to pause the presentation (blackout)
174
// Flags if speaker notes should be visible to all viewers
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
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,
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
197
// Stop auto-sliding after user input
198
autoSlideStoppable: true,
200
// Use this method for navigation when auto-sliding (defaults to navigateNext)
201
autoSlideMethod: null,
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
208
// Enable slide navigation via mouse wheel
211
// Apply a 3D roll to links on hover
214
// Hides the address bar on mobile devices
215
hideAddressBar: true,
217
// Opens links in an iframe preview overlay
218
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
222
// Exposes the reveal.js API through window.postMessage
225
// Dispatches all reveal.js events to the parent window through postMessage
226
postMessageEvents: false,
228
// Focuses body when page changes visibility to ensure keyboard shortcuts work
229
focusBodyOnPageVisibilityChange: true,
232
transition: 'slide', // none/fade/slide/convex/concave/zoom
235
transitionSpeed: 'default', // default/fast/slow
237
// Transition style for full page slide backgrounds
238
backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
240
// Parallax background image
241
parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
243
// Parallax background size
244
parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
246
// Parallax background repeat
247
parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
249
// Parallax background position
250
parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
252
// Amount of pixels to move the parallax background per slide step
253
parallaxBackgroundHorizontal: null,
254
parallaxBackgroundVertical: null,
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,
260
// Prints each fragment on a separate slide
261
pdfSeparateFragments: true,
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,
270
// Number of slides away from the current that are visible
273
// The display mode that will be used to show slides
276
// Hide cursor if inactive
277
hideInactiveCursor: true,
279
// Time before the cursor is hidden (in ms)
280
hideCursorTime: 5000,
282
// Script dependencies to load
287
// Flags if Reveal.initialize() has been called
290
// Flags if reveal.js is loaded (has dispatched the 'ready' event)
293
// Flags if the overview mode is currently active
296
// Holds the dimensions of our overview slides, including margins
297
overviewSlideWidth = null,
298
overviewSlideHeight = null,
300
// The horizontal and vertical index of the currently active slide
304
// The previous and current slide HTML elements
310
// Remember which directions that the user has navigated towards
311
hasNavigatedRight = false,
312
hasNavigatedDown = false,
314
// Slides may hold a data-state attribute which we pick up and apply
315
// as a class to the body. This list contains the combined state of
316
// all current slides.
319
// The current scale of the presentation (see width/height config)
322
// CSS transform that is currently applied to the slides container,
323
// split into two groups
324
slidesTransform = { layout: '', overview: '' },
326
// Cached references to DOM elements
329
// A list of registered reveal.js plugins
332
// List of asynchronously loaded reveal.js dependencies
333
asyncDependencies = [],
335
// Features supported by the browser, see #checkCapabilities()
338
// Client is a mobile device, see #checkCapabilities()
341
// Client is a desktop Chrome, see #checkCapabilities()
344
// Throttles mouse wheel navigation
345
lastMouseWheelStep = 0,
347
// Delays updates to the URL due to a Chrome thumbnailer bug
350
// Is the mouse pointer currently hidden from view
351
cursorHidden = false,
353
// Timeout used to determine when the cursor is inactive
354
cursorInactiveTimeout = 0,
356
// Flags if the interaction event listeners are bound
357
eventsAreBound = false,
359
// The current auto-slide duration
362
// Auto slide properties
364
autoSlideTimeout = 0,
365
autoSlideStartTime = -1,
366
autoSlidePaused = false,
368
// Holds information about the currently ongoing touch input
377
// A key:value map of shortcut keyboard keys and descriptions of
378
// the actions they trigger, generated in #configure()
379
keyboardShortcuts = {},
381
// Holds custom key code mappings
382
registeredKeyBindings = {};
385
* Starts up the presentation if the client is capable.
387
function initialize( options ) {
389
// Make sure we only initialize once
390
if( initialized === true ) return;
396
if( !features.transforms2d && !features.transforms3d ) {
397
document.body.setAttribute( 'class', 'no-transforms' );
399
// Since JS won't be running any further, we load all lazy
400
// loading elements upfront
401
var images = toArray( document.getElementsByTagName( 'img' ) ),
402
iframes = toArray( document.getElementsByTagName( 'iframe' ) );
404
var lazyLoadable = images.concat( iframes );
406
for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
407
var element = lazyLoadable[i];
408
if( element.getAttribute( 'data-src' ) ) {
409
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
410
element.removeAttribute( 'data-src' );
414
// If the browser doesn't support core features we won't be
415
// using JavaScript to control the presentation
419
// Cache references to key DOM elements
420
dom.wrapper = document.querySelector( '.reveal' );
421
dom.slides = document.querySelector( '.reveal .slides' );
423
// Force a layout when the whole page, incl fonts, has loaded
424
window.addEventListener( 'load', layout, false );
426
var query = Reveal.getQueryHash();
428
// Do not accept new dependencies via query config to avoid
429
// the potential of malicious script injection
430
if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
432
// Copy options over to our config object
433
extend( config, options );
434
extend( config, query );
436
// Hide the address bar in mobile browsers
439
// Loads dependencies and continues to #start() once done
445
* Restarts up the presentation if the client is capable.
447
function reinitialize() {
453
* Inspect the client to see what it's capable of, this
454
* should only happens once per runtime.
456
function checkCapabilities() {
458
isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
459
isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
461
var testElement = document.createElement( 'div' );
463
features.transforms3d = 'WebkitPerspective' in testElement.style ||
464
'MozPerspective' in testElement.style ||
465
'msPerspective' in testElement.style ||
466
'OPerspective' in testElement.style ||
467
'perspective' in testElement.style;
469
features.transforms2d = 'WebkitTransform' in testElement.style ||
470
'MozTransform' in testElement.style ||
471
'msTransform' in testElement.style ||
472
'OTransform' in testElement.style ||
473
'transform' in testElement.style;
475
features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
476
features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
478
features.canvas = !!document.createElement( 'canvas' ).getContext;
480
// Transitions in the overview are disabled in desktop and
482
features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
484
// Flags if we should use zoom instead of transform to scale
485
// up slides. Zoom produces crisper results but has a lot of
486
// xbrowser quirks so we only use it in whitelsited browsers.
487
features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
488
( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
493
* Loads the dependencies of reveal.js. Dependencies are
494
* defined via the configuration option 'dependencies'
495
* and will be loaded prior to starting/binding reveal.js.
496
* Some dependencies may have an 'async' flag, if so they
497
* will load after reveal.js has been started up.
504
config.dependencies.forEach( function( s ) {
505
// Load if there's no condition or the condition is truthy
506
if( !s.condition || s.condition() ) {
508
asyncDependencies.push( s );
516
if( scripts.length ) {
517
scriptsToLoad = scripts.length;
519
// Load synchronous scripts
520
scripts.forEach( function( s ) {
521
loadScript( s.src, function() {
523
if( typeof s.callback === 'function' ) s.callback();
525
if( --scriptsToLoad === 0 ) {
539
* Initializes our plugins and waits for them to be ready
542
function initPlugins() {
544
var pluginsToInitialize = Object.keys( plugins ).length;
546
// If there are no plugins, skip this step
547
if( pluginsToInitialize === 0 ) {
548
loadAsyncDependencies();
550
// ... otherwise initialize plugins
553
var afterPlugInitialized = function() {
554
if( --pluginsToInitialize === 0 ) {
555
loadAsyncDependencies();
559
for( var i in plugins ) {
561
var plugin = plugins[i];
563
// If the plugin has an 'init' method, invoke it
564
if( typeof plugin.init === 'function' ) {
565
var callback = plugin.init();
567
// If the plugin returned a Promise, wait for it
568
if( callback && typeof callback.then === 'function' ) {
569
callback.then( afterPlugInitialized );
572
afterPlugInitialized();
576
afterPlugInitialized();
586
* Loads all async reveal.js dependencies.
588
function loadAsyncDependencies() {
590
if( asyncDependencies.length ) {
591
asyncDependencies.forEach( function( s ) {
592
loadScript( s.src, s.callback );
601
* Loads a JavaScript file from the given URL and executes it.
603
* @param {string} url Address of the .js file to load
604
* @param {function} callback Method to invoke when the script
605
* has loaded and executed
607
function loadScript( url, callback ) {
609
var script = document.createElement( 'script' );
610
script.type = 'text/javascript';
611
script.async = false;
612
script.defer = false;
618
script.onload = script.onreadystatechange = function( event ) {
619
if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
621
// Kill event listeners
622
script.onload = script.onreadystatechange = script.onerror = null;
630
script.onerror = function( err ) {
632
// Kill event listeners
633
script.onload = script.onreadystatechange = script.onerror = null;
635
callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
641
// Append the script at the end of <head>
642
var head = document.querySelector( 'head' );
643
head.insertBefore( script, head.lastChild );
648
* Starts up reveal.js by binding input events and navigating
649
* to the current URL deeplink if there is one.
655
// Make sure we've got all the DOM elements we need
658
// Listen to messages posted to this window
661
// Prevent the slides from being scrolled out of view
662
setupScrollPrevention();
664
// Resets all vertical slides so that only the first is visible
665
resetVerticalSlides();
667
// Updates the presentation to match the current configuration values
670
// Read the initial hash
673
// Update all backgrounds
674
updateBackground( true );
676
// Notify listeners that the presentation is ready but use a 1ms
677
// timeout to ensure it's not fired synchronously after #initialize()
678
setTimeout( function() {
679
// Enable transitions now that we're loaded
680
dom.slides.classList.remove( 'no-transition' );
682
dom.wrapper.classList.add( 'ready' );
684
dispatchEvent( 'ready', {
687
'currentSlide': currentSlide
691
// Special setup and config is required when printing to PDF
692
if( isPrintingPDF() ) {
693
removeEventListeners();
695
// The document needs to have loaded for the PDF layout
696
// measurements to be accurate
697
if( document.readyState === 'complete' ) {
701
window.addEventListener( 'load', setupPDF );
708
* Finds and stores references to DOM elements which are
709
* required by the presentation. If a required element is
710
* not found, it is created.
712
function setupDOM() {
714
// Prevent transitions while we're loading
715
dom.slides.classList.add( 'no-transition' );
717
if( isMobileDevice ) {
718
dom.wrapper.classList.add( 'no-hover' );
721
dom.wrapper.classList.remove( 'no-hover' );
724
if( /iphone/gi.test( UA ) ) {
725
dom.wrapper.classList.add( 'ua-iphone' );
728
dom.wrapper.classList.remove( 'ua-iphone' );
731
// Background element
732
dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
735
dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
736
dom.progressbar = dom.progress.querySelector( 'span' );
739
dom.controls = createSingletonNode( dom.wrapper, 'aside', 'controls',
740
'<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' +
741
'<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' +
742
'<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' +
743
'<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>' );
746
dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
748
// Element containing notes that are visible to the audience
749
dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
750
dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
751
dom.speakerNotes.setAttribute( 'tabindex', '0' );
753
// Overlay graphic which is displayed during the paused mode
754
dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
756
dom.wrapper.setAttribute( 'role', 'application' );
758
// There can be multiple instances of controls throughout the page
759
dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
760
dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
761
dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
762
dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
763
dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
764
dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
766
// The right and down arrows in the standard reveal.js controls
767
dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' );
768
dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' );
770
dom.statusDiv = createStatusDiv();
774
* Creates a hidden div with role aria-live to announce the
775
* current slide content. Hide the div off-screen to make it
776
* available only to Assistive Technologies.
778
* @return {HTMLElement}
780
function createStatusDiv() {
782
var statusDiv = document.getElementById( 'aria-status-div' );
784
statusDiv = document.createElement( 'div' );
785
statusDiv.style.position = 'absolute';
786
statusDiv.style.height = '1px';
787
statusDiv.style.width = '1px';
788
statusDiv.style.overflow = 'hidden';
789
statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
790
statusDiv.setAttribute( 'id', 'aria-status-div' );
791
statusDiv.setAttribute( 'aria-live', 'polite' );
792
statusDiv.setAttribute( 'aria-atomic','true' );
793
dom.wrapper.appendChild( statusDiv );
800
* Converts the given HTML element into a string of text
801
* that can be announced to a screen reader. Hidden
802
* elements are excluded.
804
function getStatusText( node ) {
809
if( node.nodeType === 3 ) {
810
text += node.textContent;
813
else if( node.nodeType === 1 ) {
815
var isAriaHidden = node.getAttribute( 'aria-hidden' );
816
var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
817
if( isAriaHidden !== 'true' && !isDisplayHidden ) {
819
toArray( node.childNodes ).forEach( function( child ) {
820
text += getStatusText( child );
832
* Configures the presentation for printing to a static
835
function setupPDF() {
837
var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
839
// Dimensions of the PDF pages
840
var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
841
pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
843
// Dimensions of slides within the pages
844
var slideWidth = slideSize.width,
845
slideHeight = slideSize.height;
847
// Let the browser know what page size we want to print
848
injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
850
// Limit the size of certain elements to the dimensions of the slide
851
injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
853
document.body.classList.add( 'print-pdf' );
854
document.body.style.width = pageWidth + 'px';
855
document.body.style.height = pageHeight + 'px';
857
// Make sure stretch elements fit on slide
858
layoutSlideContents( slideWidth, slideHeight );
860
// Add each slide's index as attributes on itself, we need these
861
// indices to generate slide numbers below
862
toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
863
hslide.setAttribute( 'data-index-h', h );
865
if( hslide.classList.contains( 'stack' ) ) {
866
toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
867
vslide.setAttribute( 'data-index-h', h );
868
vslide.setAttribute( 'data-index-v', v );
873
// Slide and slide background layout
874
toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
876
// Vertical stacks are not centred since their section
878
if( slide.classList.contains( 'stack' ) === false ) {
879
// Center the slide inside of the page, giving the slide some margin
880
var left = ( pageWidth - slideWidth ) / 2,
881
top = ( pageHeight - slideHeight ) / 2;
883
var contentHeight = slide.scrollHeight;
884
var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
886
// Adhere to configured pages per slide limit
887
numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
889
// Center slides vertically
890
if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
891
top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
894
// Wrap the slide in a page element and hide its overflow
895
// so that no page ever flows onto another
896
var page = document.createElement( 'div' );
897
page.className = 'pdf-page';
898
page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
899
slide.parentNode.insertBefore( page, slide );
900
page.appendChild( slide );
902
// Position the slide inside of the page
903
slide.style.left = left + 'px';
904
slide.style.top = top + 'px';
905
slide.style.width = slideWidth + 'px';
907
if( slide.slideBackgroundElement ) {
908
page.insertBefore( slide.slideBackgroundElement, slide );
911
// Inject notes if `showNotes` is enabled
912
if( config.showNotes ) {
914
// Are there notes for this slide?
915
var notes = getSlideNotes( slide );
918
var notesSpacing = 8;
919
var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
920
var notesElement = document.createElement( 'div' );
921
notesElement.classList.add( 'speaker-notes' );
922
notesElement.classList.add( 'speaker-notes-pdf' );
923
notesElement.setAttribute( 'data-layout', notesLayout );
924
notesElement.innerHTML = notes;
926
if( notesLayout === 'separate-page' ) {
927
page.parentNode.insertBefore( notesElement, page.nextSibling );
930
notesElement.style.left = notesSpacing + 'px';
931
notesElement.style.bottom = notesSpacing + 'px';
932
notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
933
page.appendChild( notesElement );
940
// Inject slide numbers if `slideNumbers` are enabled
941
if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
942
var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
943
slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
945
var numberElement = document.createElement( 'div' );
946
numberElement.classList.add( 'slide-number' );
947
numberElement.classList.add( 'slide-number-pdf' );
948
numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
949
page.appendChild( numberElement );
952
// Copy page and show fragments one after another
953
if( config.pdfSeparateFragments ) {
955
// Each fragment 'group' is an array containing one or more
956
// fragments. Multiple fragments that appear at the same time
957
// are part of the same group.
958
var fragmentGroups = sortFragments( page.querySelectorAll( '.fragment' ), true );
960
var previousFragmentStep;
963
fragmentGroups.forEach( function( fragments ) {
965
// Remove 'current-fragment' from the previous group
966
if( previousFragmentStep ) {
967
previousFragmentStep.forEach( function( fragment ) {
968
fragment.classList.remove( 'current-fragment' );
972
// Show the fragments for the current index
973
fragments.forEach( function( fragment ) {
974
fragment.classList.add( 'visible', 'current-fragment' );
977
// Create a separate page for the current fragment state
978
var clonedPage = page.cloneNode( true );
979
page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling );
981
previousFragmentStep = fragments;
982
previousPage = clonedPage;
986
// Reset the first/original page so that all fragments are hidden
987
fragmentGroups.forEach( function( fragments ) {
988
fragments.forEach( function( fragment ) {
989
fragment.classList.remove( 'visible', 'current-fragment' );
994
// Show all fragments
996
toArray( page.querySelectorAll( '.fragment:not(.fade-out)' ) ).forEach( function( fragment ) {
997
fragment.classList.add( 'visible' );
1005
// Notify subscribers that the PDF layout is good to go
1006
dispatchEvent( 'pdf-ready' );
1011
* This is an unfortunate necessity. Some actions – such as
1012
* an input field being focused in an iframe or using the
1013
* keyboard to expand text selection beyond the bounds of
1014
* a slide – can trigger our content to be pushed out of view.
1015
* This scrolling can not be prevented by hiding overflow in
1016
* CSS (we already do) so we have to resort to repeatedly
1017
* checking if the slides have been offset :(
1019
function setupScrollPrevention() {
1021
setInterval( function() {
1022
if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
1023
dom.wrapper.scrollTop = 0;
1024
dom.wrapper.scrollLeft = 0;
1031
* Creates an HTML element and returns a reference to it.
1032
* If the element already exists the existing instance will
1035
* @param {HTMLElement} container
1036
* @param {string} tagname
1037
* @param {string} classname
1038
* @param {string} innerHTML
1040
* @return {HTMLElement}
1042
function createSingletonNode( container, tagname, classname, innerHTML ) {
1044
// Find all nodes matching the description
1045
var nodes = container.querySelectorAll( '.' + classname );
1047
// Check all matches to find one which is a direct child of
1048
// the specified container
1049
for( var i = 0; i < nodes.length; i++ ) {
1050
var testNode = nodes[i];
1051
if( testNode.parentNode === container ) {
1056
// If no node was found, create it now
1057
var node = document.createElement( tagname );
1058
node.className = classname;
1059
if( typeof innerHTML === 'string' ) {
1060
node.innerHTML = innerHTML;
1062
container.appendChild( node );
1069
* Creates the slide background elements and appends them
1070
* to the background container. One element is created per
1071
* slide no matter if the given slide has visible background.
1073
function createBackgrounds() {
1075
var printMode = isPrintingPDF();
1077
// Clear prior backgrounds
1078
dom.background.innerHTML = '';
1079
dom.background.classList.add( 'no-transition' );
1081
// Iterate over all horizontal slides
1082
toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
1084
var backgroundStack = createBackground( slideh, dom.background );
1086
// Iterate over all vertical slides
1087
toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
1089
createBackground( slidev, backgroundStack );
1091
backgroundStack.classList.add( 'stack' );
1097
// Add parallax background if specified
1098
if( config.parallaxBackgroundImage ) {
1100
dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
1101
dom.background.style.backgroundSize = config.parallaxBackgroundSize;
1102
dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat;
1103
dom.background.style.backgroundPosition = config.parallaxBackgroundPosition;
1105
// Make sure the below properties are set on the element - these properties are
1106
// needed for proper transitions to be set on the element via CSS. To remove
1107
// annoying background slide-in effect when the presentation starts, apply
1108
// these properties after short time delay
1109
setTimeout( function() {
1110
dom.wrapper.classList.add( 'has-parallax-background' );
1116
dom.background.style.backgroundImage = '';
1117
dom.wrapper.classList.remove( 'has-parallax-background' );
1124
* Creates a background for the given slide.
1126
* @param {HTMLElement} slide
1127
* @param {HTMLElement} container The element that the background
1128
* should be appended to
1129
* @return {HTMLElement} New background div
1131
function createBackground( slide, container ) {
1134
// Main slide background element
1135
var element = document.createElement( 'div' );
1136
element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
1138
// Inner background element that wraps images/videos/iframes
1139
var contentElement = document.createElement( 'div' );
1140
contentElement.className = 'slide-background-content';
1142
element.appendChild( contentElement );
1143
container.appendChild( element );
1145
slide.slideBackgroundElement = element;
1146
slide.slideBackgroundContentElement = contentElement;
1148
// Syncs the background to reflect all current background settings
1149
syncBackground( slide );
1156
* Renders all of the visual properties of a slide background
1157
* based on the various background attributes.
1159
* @param {HTMLElement} slide
1161
function syncBackground( slide ) {
1163
var element = slide.slideBackgroundElement,
1164
contentElement = slide.slideBackgroundContentElement;
1166
// Reset the prior background state in case this is not the
1168
slide.classList.remove( 'has-dark-background' );
1169
slide.classList.remove( 'has-light-background' );
1171
element.removeAttribute( 'data-loaded' );
1172
element.removeAttribute( 'data-background-hash' );
1173
element.removeAttribute( 'data-background-size' );
1174
element.removeAttribute( 'data-background-transition' );
1175
element.style.backgroundColor = '';
1177
contentElement.style.backgroundSize = '';
1178
contentElement.style.backgroundRepeat = '';
1179
contentElement.style.backgroundPosition = '';
1180
contentElement.style.backgroundImage = '';
1181
contentElement.style.opacity = '';
1182
contentElement.innerHTML = '';
1185
background: slide.getAttribute( 'data-background' ),
1186
backgroundSize: slide.getAttribute( 'data-background-size' ),
1187
backgroundImage: slide.getAttribute( 'data-background-image' ),
1188
backgroundVideo: slide.getAttribute( 'data-background-video' ),
1189
backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
1190
backgroundColor: slide.getAttribute( 'data-background-color' ),
1191
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
1192
backgroundPosition: slide.getAttribute( 'data-background-position' ),
1193
backgroundTransition: slide.getAttribute( 'data-background-transition' ),
1194
backgroundOpacity: slide.getAttribute( 'data-background-opacity' )
1197
if( data.background ) {
1198
// Auto-wrap image urls in url(...)
1199
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) {
1200
slide.setAttribute( 'data-background-image', data.background );
1203
element.style.background = data.background;
1207
// Create a hash for this combination of background settings.
1208
// This is used to determine when two slide backgrounds are
1210
if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
1211
element.setAttribute( 'data-background-hash', data.background +
1212
data.backgroundSize +
1213
data.backgroundImage +
1214
data.backgroundVideo +
1215
data.backgroundIframe +
1216
data.backgroundColor +
1217
data.backgroundRepeat +
1218
data.backgroundPosition +
1219
data.backgroundTransition +
1220
data.backgroundOpacity );
1223
// Additional and optional background properties
1224
if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
1225
if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
1226
if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
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;
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;
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;
1247
if( contrastColor ) {
1248
var rgb = colorToRgb( contrastColor );
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' );
1258
slide.classList.add( 'has-light-background' );
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:
1270
* revealWindow.postMessage( JSON.stringify({
1275
function setupPostMessage() {
1277
if( config.postMessage ) {
1278
window.addEventListener( 'message', function ( event ) {
1279
var data = event.data;
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 );
1285
// Check if the requested method can be found
1286
if( data.method && typeof Reveal[data.method] === 'function' ) {
1287
Reveal[data.method].apply( Reveal, data.args );
1296
* Applies the configuration settings from the config
1297
* object. May be called multiple times.
1299
* @param {object} options
1301
function configure( options ) {
1303
var oldTransition = config.transition;
1305
// New config options may be passed when this method
1306
// is invoked through the API after initialization
1307
if( typeof options === 'object' ) extend( config, options );
1309
// Abort if reveal.js hasn't finished loading, config
1310
// changes will be applied automatically once loading
1312
if( loaded === false ) return;
1314
var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
1316
// Remove the previously configured transition class
1317
dom.wrapper.classList.remove( oldTransition );
1319
// Force linear transition based on browser capabilities
1320
if( features.transforms3d === false ) config.transition = 'linear';
1322
dom.wrapper.classList.add( config.transition );
1324
dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
1325
dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
1327
dom.controls.style.display = config.controls ? 'block' : 'none';
1328
dom.progress.style.display = config.progress ? 'block' : 'none';
1330
dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout );
1331
dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
1333
if( config.shuffle ) {
1338
dom.wrapper.classList.add( 'rtl' );
1341
dom.wrapper.classList.remove( 'rtl' );
1344
if( config.center ) {
1345
dom.wrapper.classList.add( 'center' );
1348
dom.wrapper.classList.remove( 'center' );
1351
// Exit the paused mode if it was configured off
1352
if( config.pause === false ) {
1356
if( config.showNotes ) {
1357
dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
1360
if( config.mouseWheel ) {
1361
document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
1362
document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
1365
document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
1366
document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
1370
if( config.rollingLinks ) {
1371
enableRollingLinks();
1374
disableRollingLinks();
1377
// Auto-hide the mouse pointer when its inactive
1378
if( config.hideInactiveCursor ) {
1379
document.addEventListener( 'mousemove', onDocumentCursorActive, false );
1380
document.addEventListener( 'mousedown', onDocumentCursorActive, false );
1385
document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
1386
document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
1389
// Iframe link previews
1390
if( config.previewLinks ) {
1391
enablePreviewLinks();
1392
disablePreviewLinks( '[data-preview-link=false]' );
1395
disablePreviewLinks();
1396
enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
1399
// Remove existing auto-slide controls
1400
if( autoSlidePlayer ) {
1401
autoSlidePlayer.destroy();
1402
autoSlidePlayer = null;
1405
// Generate auto-slide controls if needed
1406
if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
1407
autoSlidePlayer = new Playback( dom.wrapper, function() {
1408
return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
1411
autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
1412
autoSlidePaused = false;
1415
// When fragments are turned off they should be visible
1416
if( config.fragments === false ) {
1417
toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
1418
element.classList.add( 'visible' );
1419
element.classList.remove( 'current-fragment' );
1424
var slideNumberDisplay = 'none';
1425
if( config.slideNumber && !isPrintingPDF() ) {
1426
if( config.showSlideNumber === 'all' ) {
1427
slideNumberDisplay = 'block';
1429
else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
1430
slideNumberDisplay = 'block';
1434
dom.slideNumber.style.display = slideNumberDisplay;
1436
// Add the navigation mode to the DOM so we can adjust styling
1437
if( config.navigationMode !== 'default' ) {
1438
dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
1441
dom.wrapper.removeAttribute( 'data-navigation-mode' );
1444
// Define our contextual list of keyboard shortcuts
1445
if( config.navigationMode === 'linear' ) {
1446
keyboardShortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
1447
keyboardShortcuts['← , ↑ , P , H , K'] = 'Previous slide';
1450
keyboardShortcuts['N , SPACE'] = 'Next slide';
1451
keyboardShortcuts['P'] = 'Previous slide';
1452
keyboardShortcuts['← , H'] = 'Navigate left';
1453
keyboardShortcuts['→ , L'] = 'Navigate right';
1454
keyboardShortcuts['↑ , K'] = 'Navigate up';
1455
keyboardShortcuts['↓ , J'] = 'Navigate down';
1458
keyboardShortcuts['Home , ⌘/CTRL ←'] = 'First slide';
1459
keyboardShortcuts['End , ⌘/CTRL →'] = 'Last slide';
1460
keyboardShortcuts['B , .'] = 'Pause';
1461
keyboardShortcuts['F'] = 'Fullscreen';
1462
keyboardShortcuts['ESC, O'] = 'Slide overview';
1469
* Binds all event listeners.
1471
function addEventListeners() {
1473
eventsAreBound = true;
1475
window.addEventListener( 'hashchange', onWindowHashChange, false );
1476
window.addEventListener( 'resize', onWindowResize, false );
1478
if( config.touch ) {
1479
if( 'onpointerdown' in window ) {
1480
// Use W3C pointer events
1481
dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
1482
dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
1483
dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
1485
else if( window.navigator.msPointerEnabled ) {
1486
// IE 10 uses prefixed version of pointer events
1487
dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
1488
dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
1489
dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
1492
// Fall back to touch events
1493
dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
1494
dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
1495
dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
1499
if( config.keyboard ) {
1500
document.addEventListener( 'keydown', onDocumentKeyDown, false );
1501
document.addEventListener( 'keypress', onDocumentKeyPress, false );
1504
if( config.progress && dom.progress ) {
1505
dom.progress.addEventListener( 'click', onProgressClicked, false );
1508
dom.pauseOverlay.addEventListener( 'click', resume, false );
1510
if( config.focusBodyOnPageVisibilityChange ) {
1511
var visibilityChange;
1513
if( 'hidden' in document ) {
1514
visibilityChange = 'visibilitychange';
1516
else if( 'msHidden' in document ) {
1517
visibilityChange = 'msvisibilitychange';
1519
else if( 'webkitHidden' in document ) {
1520
visibilityChange = 'webkitvisibilitychange';
1523
if( visibilityChange ) {
1524
document.addEventListener( visibilityChange, onPageVisibilityChange, false );
1528
// Listen to both touch and click events, in case the device
1530
var pointerEvents = [ 'touchstart', 'click' ];
1532
// Only support touch for Android, fixes double navigations in
1534
if( UA.match( /android/gi ) ) {
1535
pointerEvents = [ 'touchstart' ];
1538
pointerEvents.forEach( function( eventName ) {
1539
dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
1540
dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
1541
dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
1542
dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
1543
dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
1544
dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
1550
* Unbinds all event listeners.
1552
function removeEventListeners() {
1554
eventsAreBound = false;
1556
document.removeEventListener( 'keydown', onDocumentKeyDown, false );
1557
document.removeEventListener( 'keypress', onDocumentKeyPress, false );
1558
window.removeEventListener( 'hashchange', onWindowHashChange, false );
1559
window.removeEventListener( 'resize', onWindowResize, false );
1561
dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
1562
dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
1563
dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
1565
dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
1566
dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
1567
dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
1569
dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
1570
dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
1571
dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
1573
dom.pauseOverlay.removeEventListener( 'click', resume, false );
1575
if ( config.progress && dom.progress ) {
1576
dom.progress.removeEventListener( 'click', onProgressClicked, false );
1579
[ 'touchstart', 'click' ].forEach( function( eventName ) {
1580
dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
1581
dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
1582
dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
1583
dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
1584
dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
1585
dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
1591
* Registers a new plugin with this reveal.js instance.
1593
* reveal.js waits for all regisered plugins to initialize
1594
* before considering itself ready, as long as the plugin
1595
* is registered before calling `Reveal.initialize()`.
1597
function registerPlugin( id, plugin ) {
1599
if( plugins[id] === undefined ) {
1600
plugins[id] = plugin;
1602
// If a plugin is registered after reveal.js is loaded,
1603
// initialize it right away
1604
if( loaded && typeof plugin.init === 'function' ) {
1609
console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
1615
* Checks if a specific plugin has been registered.
1617
* @param {String} id Unique plugin identifier
1619
function hasPlugin( id ) {
1621
return !!plugins[id];
1626
* Returns the specific plugin instance, if a plugin
1627
* with the given ID has been registered.
1629
* @param {String} id Unique plugin identifier
1631
function getPlugin( id ) {
1638
* Add a custom key binding with optional description to
1639
* be added to the help screen.
1641
function addKeyBinding( binding, callback ) {
1643
if( typeof binding === 'object' && binding.keyCode ) {
1644
registeredKeyBindings[binding.keyCode] = {
1647
description: binding.description
1651
registeredKeyBindings[binding] = {
1661
* Removes the specified custom key binding.
1663
function removeKeyBinding( keyCode ) {
1665
delete registeredKeyBindings[keyCode];
1670
* Extend object a with the properties of object b.
1671
* If there's a conflict, object b takes precedence.
1676
function extend( a, b ) {
1687
* Converts the target object to an array.
1690
* @return {object[]}
1692
function toArray( o ) {
1694
return Array.prototype.slice.call( o );
1699
* Utility for deserializing a value.
1704
function deserialize( value ) {
1706
if( typeof value === 'string' ) {
1707
if( value === 'null' ) return null;
1708
else if( value === 'true' ) return true;
1709
else if( value === 'false' ) return false;
1710
else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
1718
* Measures the distance in pixels between point a
1721
* @param {object} a point with x/y properties
1722
* @param {object} b point with x/y properties
1726
function distanceBetween( a, b ) {
1731
return Math.sqrt( dx*dx + dy*dy );
1736
* Applies a CSS transform to the target element.
1738
* @param {HTMLElement} element
1739
* @param {string} transform
1741
function transformElement( element, transform ) {
1743
element.style.WebkitTransform = transform;
1744
element.style.MozTransform = transform;
1745
element.style.msTransform = transform;
1746
element.style.transform = transform;
1751
* Applies CSS transforms to the slides container. The container
1752
* is transformed from two separate sources: layout and the overview
1755
* @param {object} transforms
1757
function transformSlides( transforms ) {
1759
// Pick up new transforms from arguments
1760
if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
1761
if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
1763
// Apply the transforms to the slides container
1764
if( slidesTransform.layout ) {
1765
transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
1768
transformElement( dom.slides, slidesTransform.overview );
1774
* Injects the given CSS styles into the DOM.
1776
* @param {string} value
1778
function injectStyleSheet( value ) {
1780
var tag = document.createElement( 'style' );
1781
tag.type = 'text/css';
1782
if( tag.styleSheet ) {
1783
tag.styleSheet.cssText = value;
1786
tag.appendChild( document.createTextNode( value ) );
1788
document.getElementsByTagName( 'head' )[0].appendChild( tag );
1793
* Find the closest parent that matches the given
1796
* @param {HTMLElement} target The child element
1797
* @param {String} selector The CSS selector to match
1798
* the parents against
1800
* @return {HTMLElement} The matched parent or null
1801
* if no matching parent was found
1803
function closestParent( target, selector ) {
1805
var parent = target.parentNode;
1809
// There's some overhead doing this each time, we don't
1810
// want to rewrite the element prototype but should still
1811
// be enough to feature detect once at startup...
1812
var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
1814
// If we find a match, we're all set
1815
if( matchesMethod && matchesMethod.call( parent, selector ) ) {
1820
parent = parent.parentNode;
1829
* Converts various color input formats to an {r:0,g:0,b:0} object.
1831
* @param {string} color The string representation of a color
1833
* colorToRgb('#000');
1835
* colorToRgb('#000000');
1837
* colorToRgb('rgb(0,0,0)');
1839
* colorToRgb('rgba(0,0,0)');
1841
* @return {{r: number, g: number, b: number, [a]: number}|null}
1843
function colorToRgb( color ) {
1845
var hex3 = color.match( /^#([0-9a-f]{3})$/i );
1846
if( hex3 && hex3[1] ) {
1849
r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
1850
g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
1851
b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
1855
var hex6 = color.match( /^#([0-9a-f]{6})$/i );
1856
if( hex6 && hex6[1] ) {
1859
r: parseInt( hex6.substr( 0, 2 ), 16 ),
1860
g: parseInt( hex6.substr( 2, 2 ), 16 ),
1861
b: parseInt( hex6.substr( 4, 2 ), 16 )
1865
var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
1868
r: parseInt( rgb[1], 10 ),
1869
g: parseInt( rgb[2], 10 ),
1870
b: parseInt( rgb[3], 10 )
1874
var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
1877
r: parseInt( rgba[1], 10 ),
1878
g: parseInt( rgba[2], 10 ),
1879
b: parseInt( rgba[3], 10 ),
1880
a: parseFloat( rgba[4] )
1889
* Calculates brightness on a scale of 0-255.
1891
* @param {string} color See colorToRgb for supported formats.
1892
* @see {@link colorToRgb}
1894
function colorBrightness( color ) {
1896
if( typeof color === 'string' ) color = colorToRgb( color );
1899
return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
1907
* Returns the remaining height within the parent of the
1910
* remaining height = [ configured parent height ] - [ current parent height ]
1912
* @param {HTMLElement} element
1913
* @param {number} [height]
1915
function getRemainingHeight( element, height ) {
1917
height = height || 0;
1920
var newHeight, oldHeight = element.style.height;
1922
// Change the .stretch element height to 0 in order find the height of all
1923
// the other elements
1924
element.style.height = '0px';
1926
// In Overview mode, the parent (.slide) height is set of 700px.
1927
// Restore it temporarily to its natural height.
1928
element.parentNode.style.height = 'auto';
1930
newHeight = height - element.parentNode.offsetHeight;
1932
// Restore the old height, just in case
1933
element.style.height = oldHeight + 'px';
1935
// Clear the parent (.slide) height. .removeProperty works in IE9+
1936
element.parentNode.style.removeProperty('height');
1946
* Checks if this instance is being used to print a PDF.
1948
function isPrintingPDF() {
1950
return ( /print-pdf/gi ).test( window.location.search );
1955
* Hides the address bar if we're on a mobile device.
1957
function hideAddressBar() {
1959
if( config.hideAddressBar && isMobileDevice ) {
1960
// Events that should trigger the address bar to hide
1961
window.addEventListener( 'load', removeAddressBar, false );
1962
window.addEventListener( 'orientationchange', removeAddressBar, false );
1968
* Causes the address bar to hide on mobile devices,
1969
* more vertical space ftw.
1971
function removeAddressBar() {
1973
setTimeout( function() {
1974
window.scrollTo( 0, 1 );
1980
* Dispatches an event of the specified type from the
1981
* reveal DOM element.
1983
function dispatchEvent( type, args ) {
1985
var event = document.createEvent( 'HTMLEvents', 1, 2 );
1986
event.initEvent( type, true, true );
1987
extend( event, args );
1988
dom.wrapper.dispatchEvent( event );
1990
// If we're in an iframe, post each reveal.js event to the
1991
// parent window. Used by the notes plugin
1992
if( config.postMessageEvents && window.parent !== window.self ) {
1993
window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
1999
* Wrap all links in 3D goodness.
2001
function enableRollingLinks() {
2003
if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
2004
var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
2006
for( var i = 0, len = anchors.length; i < len; i++ ) {
2007
var anchor = anchors[i];
2009
if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
2010
var span = document.createElement('span');
2011
span.setAttribute('data-title', anchor.text);
2012
span.innerHTML = anchor.innerHTML;
2014
anchor.classList.add( 'roll' );
2015
anchor.innerHTML = '';
2016
anchor.appendChild(span);
2024
* Unwrap all 3D links.
2026
function disableRollingLinks() {
2028
var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
2030
for( var i = 0, len = anchors.length; i < len; i++ ) {
2031
var anchor = anchors[i];
2032
var span = anchor.querySelector( 'span' );
2035
anchor.classList.remove( 'roll' );
2036
anchor.innerHTML = span.innerHTML;
2043
* Bind preview frame links.
2045
* @param {string} [selector=a] - selector for anchors
2047
function enablePreviewLinks( selector ) {
2049
var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
2051
anchors.forEach( function( element ) {
2052
if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
2053
element.addEventListener( 'click', onPreviewLinkClicked, false );
2060
* Unbind preview frame links.
2062
function disablePreviewLinks( selector ) {
2064
var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
2066
anchors.forEach( function( element ) {
2067
if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
2068
element.removeEventListener( 'click', onPreviewLinkClicked, false );
2075
* Opens a preview window for the target URL.
2077
* @param {string} url - url for preview iframe src
2079
function showPreview( url ) {
2083
dom.overlay = document.createElement( 'div' );
2084
dom.overlay.classList.add( 'overlay' );
2085
dom.overlay.classList.add( 'overlay-preview' );
2086
dom.wrapper.appendChild( dom.overlay );
2088
dom.overlay.innerHTML = [
2090
'<a class="close" href="#"><span class="icon"></span></a>',
2091
'<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
2093
'<div class="spinner"></div>',
2094
'<div class="viewport">',
2095
'<iframe src="'+ url +'"></iframe>',
2096
'<small class="viewport-inner">',
2097
'<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
2102
dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
2103
dom.overlay.classList.add( 'loaded' );
2106
dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
2108
event.preventDefault();
2111
dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
2115
setTimeout( function() {
2116
dom.overlay.classList.add( 'visible' );
2122
* Open or close help overlay window.
2124
* @param {Boolean} [override] Flag which overrides the
2125
* toggle logic and forcibly sets the desired state. True means
2126
* help is open, false means it's closed.
2128
function toggleHelp( override ){
2130
if( typeof override === 'boolean' ) {
2131
override ? showHelp() : closeOverlay();
2144
* Opens an overlay window with help material.
2146
function showHelp() {
2152
dom.overlay = document.createElement( 'div' );
2153
dom.overlay.classList.add( 'overlay' );
2154
dom.overlay.classList.add( 'overlay-help' );
2155
dom.wrapper.appendChild( dom.overlay );
2157
var html = '<p class="title">Keyboard Shortcuts</p><br/>';
2159
html += '<table><th>KEY</th><th>ACTION</th>';
2160
for( var key in keyboardShortcuts ) {
2161
html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
2164
// Add custom key bindings that have associated descriptions
2165
for( var binding in registeredKeyBindings ) {
2166
if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
2167
html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
2173
dom.overlay.innerHTML = [
2175
'<a class="close" href="#"><span class="icon"></span></a>',
2177
'<div class="viewport">',
2178
'<div class="viewport-inner">'+ html +'</div>',
2182
dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
2184
event.preventDefault();
2187
setTimeout( function() {
2188
dom.overlay.classList.add( 'visible' );
2196
* Closes any currently open overlay.
2198
function closeOverlay() {
2201
dom.overlay.parentNode.removeChild( dom.overlay );
2208
* Applies JavaScript-controlled layout rules to the
2213
if( dom.wrapper && !isPrintingPDF() ) {
2215
if( !config.disableLayout ) {
2217
// On some mobile devices '100vh' is taller than the visible
2218
// viewport which leads to part of the presentation being
2219
// cut off. To work around this we define our own '--vh' custom
2220
// property where 100x adds up to the correct height.
2222
// https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
2223
if( isMobileDevice ) {
2224
document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
2227
var size = getComputedSlideSize();
2229
var oldScale = scale;
2231
// Layout the contents of the slides
2232
layoutSlideContents( config.width, config.height );
2234
dom.slides.style.width = size.width + 'px';
2235
dom.slides.style.height = size.height + 'px';
2237
// Determine scale of content to fit within available space
2238
scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
2240
// Respect max/min scale settings
2241
scale = Math.max( scale, config.minScale );
2242
scale = Math.min( scale, config.maxScale );
2244
// Don't apply any scaling styles if scale is 1
2246
dom.slides.style.zoom = '';
2247
dom.slides.style.left = '';
2248
dom.slides.style.top = '';
2249
dom.slides.style.bottom = '';
2250
dom.slides.style.right = '';
2251
transformSlides( { layout: '' } );
2254
// Prefer zoom for scaling up so that content remains crisp.
2255
// Don't use zoom to scale down since that can lead to shifts
2256
// in text layout/line breaks.
2257
if( scale > 1 && features.zoom ) {
2258
dom.slides.style.zoom = scale;
2259
dom.slides.style.left = '';
2260
dom.slides.style.top = '';
2261
dom.slides.style.bottom = '';
2262
dom.slides.style.right = '';
2263
transformSlides( { layout: '' } );
2265
// Apply scale transform as a fallback
2267
dom.slides.style.zoom = '';
2268
dom.slides.style.left = '50%';
2269
dom.slides.style.top = '50%';
2270
dom.slides.style.bottom = 'auto';
2271
dom.slides.style.right = 'auto';
2272
transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
2276
// Select all slides, vertical and horizontal
2277
var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
2279
for( var i = 0, len = slides.length; i < len; i++ ) {
2280
var slide = slides[ i ];
2282
// Don't bother updating invisible slides
2283
if( slide.style.display === 'none' ) {
2287
if( config.center || slide.classList.contains( 'center' ) ) {
2288
// Vertical stacks are not centred since their section
2290
if( slide.classList.contains( 'stack' ) ) {
2291
slide.style.top = 0;
2294
slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
2298
slide.style.top = '';
2303
if( oldScale !== scale ) {
2304
dispatchEvent( 'resize', {
2305
'oldScale': oldScale,
2315
if( isOverview() ) {
2324
* Applies layout logic to the contents of all slides in
2327
* @param {string|number} width
2328
* @param {string|number} height
2330
function layoutSlideContents( width, height ) {
2332
// Handle sizing of elements with the 'stretch' class
2333
toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
2335
// Determine how much vertical space we can use
2336
var remainingHeight = getRemainingHeight( element, height );
2338
// Consider the aspect ratio of media elements
2339
if( /(img|video)/gi.test( element.nodeName ) ) {
2340
var nw = element.naturalWidth || element.videoWidth,
2341
nh = element.naturalHeight || element.videoHeight;
2343
var es = Math.min( width / nw, remainingHeight / nh );
2345
element.style.width = ( nw * es ) + 'px';
2346
element.style.height = ( nh * es ) + 'px';
2350
element.style.width = width + 'px';
2351
element.style.height = remainingHeight + 'px';
2359
* Calculates the computed pixel size of our slides. These
2360
* values are based on the width and height configuration
2363
* @param {number} [presentationWidth=dom.wrapper.offsetWidth]
2364
* @param {number} [presentationHeight=dom.wrapper.offsetHeight]
2366
function getComputedSlideSize( presentationWidth, presentationHeight ) {
2370
width: config.width,
2371
height: config.height,
2373
// Presentation size
2374
presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
2375
presentationHeight: presentationHeight || dom.wrapper.offsetHeight
2378
// Reduce available space by margin
2379
size.presentationWidth -= ( size.presentationWidth * config.margin );
2380
size.presentationHeight -= ( size.presentationHeight * config.margin );
2382
// Slide width may be a percentage of available width
2383
if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
2384
size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
2387
// Slide height may be a percentage of available height
2388
if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
2389
size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
2397
* Stores the vertical index of a stack so that the same
2398
* vertical slide can be selected when navigating to and
2401
* @param {HTMLElement} stack The vertical stack element
2402
* @param {string|number} [v=0] Index to memorize
2404
function setPreviousVerticalIndex( stack, v ) {
2406
if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
2407
stack.setAttribute( 'data-previous-indexv', v || 0 );
2413
* Retrieves the vertical index which was stored using
2414
* #setPreviousVerticalIndex() or 0 if no previous index
2417
* @param {HTMLElement} stack The vertical stack element
2419
function getPreviousVerticalIndex( stack ) {
2421
if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
2422
// Prefer manually defined start-indexv
2423
var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
2425
return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
2433
* Displays the overview of slides (quick nav) by scaling
2434
* down and arranging all slide elements.
2436
function activateOverview() {
2438
// Only proceed if enabled in config
2439
if( config.overview && !isOverview() ) {
2443
dom.wrapper.classList.add( 'overview' );
2444
dom.wrapper.classList.remove( 'overview-deactivating' );
2446
if( features.overviewTransitions ) {
2447
setTimeout( function() {
2448
dom.wrapper.classList.add( 'overview-animated' );
2452
// Don't auto-slide while in overview mode
2455
// Move the backgrounds element into the slide container to
2456
// that the same scaling is applied
2457
dom.slides.appendChild( dom.background );
2459
// Clicking on an overview slide navigates to it
2460
toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
2461
if( !slide.classList.contains( 'stack' ) ) {
2462
slide.addEventListener( 'click', onOverviewSlideClicked, true );
2466
// Calculate slide sizes
2468
var slideSize = getComputedSlideSize();
2469
overviewSlideWidth = slideSize.width + margin;
2470
overviewSlideHeight = slideSize.height + margin;
2472
// Reverse in RTL mode
2474
overviewSlideWidth = -overviewSlideWidth;
2477
updateSlidesVisibility();
2483
// Notify observers of the overview showing
2484
dispatchEvent( 'overviewshown', {
2487
'currentSlide': currentSlide
2495
* Uses CSS transforms to position all slides in a grid for
2496
* display inside of the overview mode.
2498
function layoutOverview() {
2501
toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
2502
hslide.setAttribute( 'data-index-h', h );
2503
transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
2505
if( hslide.classList.contains( 'stack' ) ) {
2507
toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
2508
vslide.setAttribute( 'data-index-h', h );
2509
vslide.setAttribute( 'data-index-v', v );
2511
transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
2517
// Layout slide backgrounds
2518
toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
2519
transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
2521
toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
2522
transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
2529
* Moves the overview viewport to the current slides.
2530
* Called each time the current slide changes.
2532
function updateOverview() {
2534
var vmin = Math.min( window.innerWidth, window.innerHeight );
2535
var scale = Math.max( vmin / 5, 150 ) / vmin;
2539
'scale('+ scale +')',
2540
'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
2541
'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
2548
* Exits the slide overview and enters the currently
2551
function deactivateOverview() {
2553
// Only proceed if enabled in config
2554
if( config.overview ) {
2558
dom.wrapper.classList.remove( 'overview' );
2559
dom.wrapper.classList.remove( 'overview-animated' );
2561
// Temporarily add a class so that transitions can do different things
2562
// depending on whether they are exiting/entering overview, or just
2563
// moving from slide to slide
2564
dom.wrapper.classList.add( 'overview-deactivating' );
2566
setTimeout( function () {
2567
dom.wrapper.classList.remove( 'overview-deactivating' );
2570
// Move the background element back out
2571
dom.wrapper.appendChild( dom.background );
2573
// Clean up changes made to slides
2574
toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
2575
transformElement( slide, '' );
2577
slide.removeEventListener( 'click', onOverviewSlideClicked, true );
2580
// Clean up changes made to backgrounds
2581
toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
2582
transformElement( background, '' );
2585
transformSlides( { overview: '' } );
2587
slide( indexh, indexv );
2593
// Notify observers of the overview hiding
2594
dispatchEvent( 'overviewhidden', {
2597
'currentSlide': currentSlide
2604
* Toggles the slide overview mode on and off.
2606
* @param {Boolean} [override] Flag which overrides the
2607
* toggle logic and forcibly sets the desired state. True means
2608
* overview is open, false means it's closed.
2610
function toggleOverview( override ) {
2612
if( typeof override === 'boolean' ) {
2613
override ? activateOverview() : deactivateOverview();
2616
isOverview() ? deactivateOverview() : activateOverview();
2622
* Checks if the overview is currently active.
2624
* @return {Boolean} true if the overview is active,
2627
function isOverview() {
2634
* Return a hash URL that will resolve to the current slide location.
2636
function locationHash() {
2640
// Attempt to create a named link based on the slide's ID
2641
var id = currentSlide ? currentSlide.getAttribute( 'id' ) : null;
2643
id = encodeURIComponent( id );
2647
if( config.fragmentInURL ) {
2648
indexf = getIndices().f;
2651
// If the current slide has an ID, use that as a named link,
2652
// but we don't support named links with a fragment index
2653
if( typeof id === 'string' && id.length && indexf === undefined ) {
2656
// Otherwise use the /h/v index
2658
var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
2659
if( indexh > 0 || indexv > 0 || indexf !== undefined ) url += indexh + hashIndexBase;
2660
if( indexv > 0 || indexf !== undefined ) url += '/' + (indexv + hashIndexBase );
2661
if( indexf !== undefined ) url += '/' + indexf;
2669
* Checks if the current or specified slide is vertical
2670
* (nested within another slide).
2672
* @param {HTMLElement} [slide=currentSlide] The slide to check
2676
function isVerticalSlide( slide ) {
2678
// Prefer slide argument, otherwise use current slide
2679
slide = slide ? slide : currentSlide;
2681
return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
2686
* Handling the fullscreen functionality via the fullscreen API
2688
* @see http://fullscreen.spec.whatwg.org/
2689
* @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
2691
function enterFullscreen() {
2693
var element = document.documentElement;
2695
// Check which implementation is available
2696
var requestMethod = element.requestFullscreen ||
2697
element.webkitRequestFullscreen ||
2698
element.webkitRequestFullScreen ||
2699
element.mozRequestFullScreen ||
2700
element.msRequestFullscreen;
2702
if( requestMethod ) {
2703
requestMethod.apply( element );
2709
* Shows the mouse pointer after it has been hidden with
2712
function showCursor() {
2714
if( cursorHidden ) {
2715
cursorHidden = false;
2716
dom.wrapper.style.cursor = '';
2722
* Hides the mouse pointer when it's on top of the .reveal
2725
function hideCursor() {
2727
if( cursorHidden === false ) {
2728
cursorHidden = true;
2729
dom.wrapper.style.cursor = 'none';
2735
* Enters the paused mode which fades everything on screen to
2740
if( config.pause ) {
2741
var wasPaused = dom.wrapper.classList.contains( 'paused' );
2744
dom.wrapper.classList.add( 'paused' );
2746
if( wasPaused === false ) {
2747
dispatchEvent( 'paused' );
2754
* Exits from the paused mode.
2758
var wasPaused = dom.wrapper.classList.contains( 'paused' );
2759
dom.wrapper.classList.remove( 'paused' );
2764
dispatchEvent( 'resumed' );
2770
* Toggles the paused mode on and off.
2772
function togglePause( override ) {
2774
if( typeof override === 'boolean' ) {
2775
override ? pause() : resume();
2778
isPaused() ? resume() : pause();
2784
* Checks if we are currently in the paused mode.
2788
function isPaused() {
2790
return dom.wrapper.classList.contains( 'paused' );
2795
* Toggles the auto slide mode on and off.
2797
* @param {Boolean} [override] Flag which sets the desired state.
2798
* True means autoplay starts, false means it stops.
2801
function toggleAutoSlide( override ) {
2803
if( typeof override === 'boolean' ) {
2804
override ? resumeAutoSlide() : pauseAutoSlide();
2808
autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
2814
* Checks if the auto slide mode is currently on.
2818
function isAutoSliding() {
2820
return !!( autoSlide && !autoSlidePaused );
2825
* Steps from the current point in the presentation to the
2826
* slide which matches the specified horizontal and vertical
2829
* @param {number} [h=indexh] Horizontal index of the target slide
2830
* @param {number} [v=indexv] Vertical index of the target slide
2831
* @param {number} [f] Index of a fragment within the
2832
* target slide to activate
2833
* @param {number} [o] Origin for use in multimaster environments
2835
function slide( h, v, f, o ) {
2837
// Remember where we were at before
2838
previousSlide = currentSlide;
2840
// Query all horizontal slides in the deck
2841
var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
2843
// Abort if there are no slides
2844
if( horizontalSlides.length === 0 ) return;
2846
// If no vertical index is specified and the upcoming slide is a
2847
// stack, resume at its previous vertical index
2848
if( v === undefined && !isOverview() ) {
2849
v = getPreviousVerticalIndex( horizontalSlides[ h ] );
2852
// If we were on a vertical stack, remember what vertical index
2853
// it was on so we can resume at the same position when returning
2854
if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
2855
setPreviousVerticalIndex( previousSlide.parentNode, indexv );
2858
// Remember the state before this slide
2859
var stateBefore = state.concat();
2861
// Reset the state array
2864
var indexhBefore = indexh || 0,
2865
indexvBefore = indexv || 0;
2867
// Activate and transition to the new slide
2868
indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
2869
indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
2871
// Update the visibility of slides now that the indices have changed
2872
updateSlidesVisibility();
2876
// Update the overview if it's currently active
2877
if( isOverview() ) {
2881
// Find the current horizontal slide and any possible vertical slides
2883
var currentHorizontalSlide = horizontalSlides[ indexh ],
2884
currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
2886
// Store references to the previous and current slides
2887
currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
2889
// Show fragment, if specified
2890
if( typeof f !== 'undefined' ) {
2891
navigateFragment( f );
2894
// Dispatch an event if the slide changed
2895
var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
2896
if (!slideChanged) {
2897
// Ensure that the previous slide is never the same as the current
2898
previousSlide = null;
2901
// Solves an edge case where the previous slide maintains the
2902
// 'present' class when navigating between adjacent vertical
2904
if( previousSlide && previousSlide !== currentSlide ) {
2905
previousSlide.classList.remove( 'present' );
2906
previousSlide.setAttribute( 'aria-hidden', 'true' );
2908
// Reset all slides upon navigate to home
2910
if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
2911
// Launch async task
2912
setTimeout( function () {
2913
var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
2914
for( i in slides ) {
2917
setPreviousVerticalIndex( slides[i], 0 );
2924
// Apply the new state
2925
stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
2926
// Check if this state existed on the previous slide. If it
2927
// did, we will avoid adding it repeatedly
2928
for( var j = 0; j < stateBefore.length; j++ ) {
2929
if( stateBefore[j] === state[i] ) {
2930
stateBefore.splice( j, 1 );
2935
document.documentElement.classList.add( state[i] );
2937
// Dispatch custom event matching the state's name
2938
dispatchEvent( state[i] );
2941
// Clean up the remains of the previous state
2942
while( stateBefore.length ) {
2943
document.documentElement.classList.remove( stateBefore.pop() );
2946
if( slideChanged ) {
2947
dispatchEvent( 'slidechanged', {
2950
'previousSlide': previousSlide,
2951
'currentSlide': currentSlide,
2956
// Handle embedded content
2957
if( slideChanged || !previousSlide ) {
2958
stopEmbeddedContent( previousSlide );
2959
startEmbeddedContent( currentSlide );
2962
// Announce the current slide contents, for screen readers
2963
dom.statusDiv.textContent = getStatusText( currentSlide );
2969
updateSlideNumber();
2973
// Update the URL hash
2981
* Syncs the presentation with the current DOM. Useful
2982
* when new slides or control elements are added or when
2983
* the configuration has changed.
2987
// Subscribe to input
2988
removeEventListeners();
2989
addEventListeners();
2991
// Force a layout to make sure the current config is accounted for
2994
// Reflect the current autoSlide value
2995
autoSlide = config.autoSlide;
2997
// Start auto-sliding if it's enabled
3000
// Re-create the slide backgrounds
3001
createBackgrounds();
3003
// Write the current hash to the URL
3010
updateSlideNumber();
3011
updateSlidesVisibility();
3012
updateBackground( true );
3013
updateNotesVisibility();
3016
formatEmbeddedContent();
3018
// Start or stop embedded content depending on global config
3019
if( config.autoPlayMedia === false ) {
3020
stopEmbeddedContent( currentSlide, { unloadIframes: false } );
3023
startEmbeddedContent( currentSlide );
3026
if( isOverview() ) {
3033
* Updates reveal.js to keep in sync with new slide attributes. For
3034
* example, if you add a new `data-background-image` you can call
3035
* this to have reveal.js render the new background image.
3037
* Similar to #sync() but more efficient when you only need to
3038
* refresh a specific slide.
3040
* @param {HTMLElement} slide
3042
function syncSlide( slide ) {
3044
// Default to the current slide
3045
slide = slide || currentSlide;
3047
syncBackground( slide );
3048
syncFragments( slide );
3058
* Formats the fragments on the given slide so that they have
3059
* valid indices. Call this if fragments are changed in the DOM
3060
* after reveal.js has already initialized.
3062
* @param {HTMLElement} slide
3063
* @return {Array} a list of the HTML fragments that were synced
3065
function syncFragments( slide ) {
3067
// Default to the current slide
3068
slide = slide || currentSlide;
3070
return sortFragments( slide.querySelectorAll( '.fragment' ) );
3075
* Resets all vertical slides so that only the first
3078
function resetVerticalSlides() {
3080
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3081
horizontalSlides.forEach( function( horizontalSlide ) {
3083
var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
3084
verticalSlides.forEach( function( verticalSlide, y ) {
3087
verticalSlide.classList.remove( 'present' );
3088
verticalSlide.classList.remove( 'past' );
3089
verticalSlide.classList.add( 'future' );
3090
verticalSlide.setAttribute( 'aria-hidden', 'true' );
3100
* Sorts and formats all of fragments in the
3103
function sortAllFragments() {
3105
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3106
horizontalSlides.forEach( function( horizontalSlide ) {
3108
var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
3109
verticalSlides.forEach( function( verticalSlide, y ) {
3111
sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
3115
if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
3122
* Randomly shuffles all slides in the deck.
3124
function shuffle() {
3126
var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3128
slides.forEach( function( slide ) {
3130
// Insert this slide next to another random slide. This may
3131
// cause the slide to insert before itself but that's fine.
3132
dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
3139
* Updates one dimension of slides by showing the slide
3140
* with the specified index.
3142
* @param {string} selector A CSS selector that will fetch
3143
* the group of slides we are working with
3144
* @param {number} index The index of the slide that should be
3147
* @return {number} The index of the slide that is now shown,
3148
* might differ from the passed in index if it was out of
3151
function updateSlides( selector, index ) {
3153
// Select all slides and convert the NodeList result to
3155
var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
3156
slidesLength = slides.length;
3158
var printMode = isPrintingPDF();
3160
if( slidesLength ) {
3162
// Should the index loop?
3164
index %= slidesLength;
3167
index = slidesLength + index;
3171
// Enforce max and minimum index bounds
3172
index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
3174
for( var i = 0; i < slidesLength; i++ ) {
3175
var element = slides[i];
3177
var reverse = config.rtl && !isVerticalSlide( element );
3179
element.classList.remove( 'past' );
3180
element.classList.remove( 'present' );
3181
element.classList.remove( 'future' );
3183
// http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
3184
element.setAttribute( 'hidden', '' );
3185
element.setAttribute( 'aria-hidden', 'true' );
3187
// If this element contains vertical slides
3188
if( element.querySelector( 'section' ) ) {
3189
element.classList.add( 'stack' );
3192
// If we're printing static slides, all slides are "present"
3194
element.classList.add( 'present' );
3199
// Any element previous to index is given the 'past' class
3200
element.classList.add( reverse ? 'future' : 'past' );
3202
if( config.fragments ) {
3203
// Show all fragments in prior slides
3204
toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
3205
fragment.classList.add( 'visible' );
3206
fragment.classList.remove( 'current-fragment' );
3210
else if( i > index ) {
3211
// Any element subsequent to index is given the 'future' class
3212
element.classList.add( reverse ? 'past' : 'future' );
3214
if( config.fragments ) {
3215
// Hide all fragments in future slides
3216
toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
3217
fragment.classList.remove( 'visible' );
3218
fragment.classList.remove( 'current-fragment' );
3224
// Mark the current slide as present
3225
slides[index].classList.add( 'present' );
3226
slides[index].removeAttribute( 'hidden' );
3227
slides[index].removeAttribute( 'aria-hidden' );
3229
// If this slide has a state associated with it, add it
3230
// onto the current state of the deck
3231
var slideState = slides[index].getAttribute( 'data-state' );
3233
state = state.concat( slideState.split( ' ' ) );
3238
// Since there are no slides we can't be anywhere beyond the
3248
* Optimization method; hide all slides that are far away
3249
* from the present slide.
3251
function updateSlidesVisibility() {
3253
// Select all slides and convert the NodeList result to
3255
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
3256
horizontalSlidesLength = horizontalSlides.length,
3260
if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
3262
// The number of steps away from the present slide that will
3264
var viewDistance = isOverview() ? 10 : config.viewDistance;
3266
// Limit view distance on weaker devices
3267
if( isMobileDevice ) {
3268
viewDistance = isOverview() ? 6 : 2;
3271
// All slides need to be visible when exporting to PDF
3272
if( isPrintingPDF() ) {
3273
viewDistance = Number.MAX_VALUE;
3276
for( var x = 0; x < horizontalSlidesLength; x++ ) {
3277
var horizontalSlide = horizontalSlides[x];
3279
var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
3280
verticalSlidesLength = verticalSlides.length;
3282
// Determine how far away this slide is from the present
3283
distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
3285
// If the presentation is looped, distance should measure
3286
// 1 between the first and last slides
3288
distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
3291
// Show the horizontal slide if it's within the view distance
3292
if( distanceX < viewDistance ) {
3293
loadSlide( horizontalSlide );
3296
unloadSlide( horizontalSlide );
3299
if( verticalSlidesLength ) {
3301
var oy = getPreviousVerticalIndex( horizontalSlide );
3303
for( var y = 0; y < verticalSlidesLength; y++ ) {
3304
var verticalSlide = verticalSlides[y];
3306
distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
3308
if( distanceX + distanceY < viewDistance ) {
3309
loadSlide( verticalSlide );
3312
unloadSlide( verticalSlide );
3319
// Flag if there are ANY vertical slides, anywhere in the deck
3320
if( dom.wrapper.querySelectorAll( '.slides>section>section' ).length ) {
3321
dom.wrapper.classList.add( 'has-vertical-slides' );
3324
dom.wrapper.classList.remove( 'has-vertical-slides' );
3327
// Flag if there are ANY horizontal slides, anywhere in the deck
3328
if( dom.wrapper.querySelectorAll( '.slides>section' ).length > 1 ) {
3329
dom.wrapper.classList.add( 'has-horizontal-slides' );
3332
dom.wrapper.classList.remove( 'has-horizontal-slides' );
3340
* Pick up notes from the current slide and display them
3343
* @see {@link config.showNotes}
3345
function updateNotes() {
3347
if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
3349
dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
3356
* Updates the visibility of the speaker notes sidebar that
3357
* is used to share annotated slides. The notes sidebar is
3358
* only visible if showNotes is true and there are notes on
3359
* one or more slides in the deck.
3361
function updateNotesVisibility() {
3363
if( config.showNotes && hasNotes() ) {
3364
dom.wrapper.classList.add( 'show-notes' );
3367
dom.wrapper.classList.remove( 'show-notes' );
3373
* Checks if there are speaker notes for ANY slide in the
3376
function hasNotes() {
3378
return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0;
3383
* Updates the progress bar to reflect the current slide.
3385
function updateProgress() {
3387
// Update progress if enabled
3388
if( config.progress && dom.progressbar ) {
3390
dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
3398
* Updates the slide number to match the current slide.
3400
function updateSlideNumber() {
3402
// Update slide number if enabled
3403
if( config.slideNumber && dom.slideNumber ) {
3408
if( typeof config.slideNumber === 'function' ) {
3409
value = config.slideNumber();
3412
// Check if a custom number format is available
3413
if( typeof config.slideNumber === 'string' ) {
3414
format = config.slideNumber;
3417
// If there are ONLY vertical slides in this deck, always use
3418
// a flattened slide number
3419
if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
3426
value.push( getSlidePastCount() + 1 );
3429
value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
3432
value.push( indexh + 1 );
3433
if( isVerticalSlide() ) value.push( '/', indexv + 1 );
3436
value.push( indexh + 1 );
3437
if( isVerticalSlide() ) value.push( '.', indexv + 1 );
3441
dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
3447
* Applies HTML formatting to a slide number before it's
3448
* written to the DOM.
3450
* @param {number} a Current slide
3451
* @param {string} delimiter Character to separate slide numbers
3452
* @param {(number|*)} b Total slides
3453
* @return {string} HTML string fragment
3455
function formatSlideNumber( a, delimiter, b ) {
3457
var url = '#' + locationHash();
3458
if( typeof b === 'number' && !isNaN( b ) ) {
3459
return '<a href="' + url + '">' +
3460
'<span class="slide-number-a">'+ a +'</span>' +
3461
'<span class="slide-number-delimiter">'+ delimiter +'</span>' +
3462
'<span class="slide-number-b">'+ b +'</span>' +
3466
return '<a href="' + url + '">' +
3467
'<span class="slide-number-a">'+ a +'</span>' +
3474
* Updates the state of all control/navigation arrows.
3476
function updateControls() {
3478
var routes = availableRoutes();
3479
var fragments = availableFragments();
3481
// Remove the 'enabled' class from all directions
3482
dom.controlsLeft.concat( dom.controlsRight )
3483
.concat( dom.controlsUp )
3484
.concat( dom.controlsDown )
3485
.concat( dom.controlsPrev )
3486
.concat( dom.controlsNext ).forEach( function( node ) {
3487
node.classList.remove( 'enabled' );
3488
node.classList.remove( 'fragmented' );
3490
// Set 'disabled' attribute on all directions
3491
node.setAttribute( 'disabled', 'disabled' );
3494
// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
3495
if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3496
if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3497
if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3498
if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3500
// Prev/next buttons
3501
if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3502
if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
3504
// Highlight fragment directions
3505
if( currentSlide ) {
3507
// Always apply fragment decorator to prev/next buttons
3508
if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3509
if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3511
// Apply fragment decorators to directional buttons based on
3512
// what slide axis they are in
3513
if( isVerticalSlide( currentSlide ) ) {
3514
if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3515
if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3518
if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3519
if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
3524
if( config.controlsTutorial ) {
3526
// Highlight control arrows with an animation to ensure
3527
// that the viewer knows how to navigate
3528
if( !hasNavigatedDown && routes.down ) {
3529
dom.controlsDownArrow.classList.add( 'highlight' );
3532
dom.controlsDownArrow.classList.remove( 'highlight' );
3534
if( !hasNavigatedRight && routes.right && indexv === 0 ) {
3535
dom.controlsRightArrow.classList.add( 'highlight' );
3538
dom.controlsRightArrow.classList.remove( 'highlight' );
3547
* Updates the background elements to reflect the current
3550
* @param {boolean} includeAll If true, the backgrounds of
3551
* all vertical slides (not just the present) will be updated.
3553
function updateBackground( includeAll ) {
3555
var currentBackground = null;
3557
// Reverse past/future classes when in RTL mode
3558
var horizontalPast = config.rtl ? 'future' : 'past',
3559
horizontalFuture = config.rtl ? 'past' : 'future';
3561
// Update the classes of all backgrounds to match the
3562
// states of their slides (past/present/future)
3563
toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
3565
backgroundh.classList.remove( 'past' );
3566
backgroundh.classList.remove( 'present' );
3567
backgroundh.classList.remove( 'future' );
3570
backgroundh.classList.add( horizontalPast );
3572
else if ( h > indexh ) {
3573
backgroundh.classList.add( horizontalFuture );
3576
backgroundh.classList.add( 'present' );
3578
// Store a reference to the current background element
3579
currentBackground = backgroundh;
3582
if( includeAll || h === indexh ) {
3583
toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
3585
backgroundv.classList.remove( 'past' );
3586
backgroundv.classList.remove( 'present' );
3587
backgroundv.classList.remove( 'future' );
3590
backgroundv.classList.add( 'past' );
3592
else if ( v > indexv ) {
3593
backgroundv.classList.add( 'future' );
3596
backgroundv.classList.add( 'present' );
3598
// Only if this is the present horizontal and vertical slide
3599
if( h === indexh ) currentBackground = backgroundv;
3607
// Stop content inside of previous backgrounds
3608
if( previousBackground ) {
3610
stopEmbeddedContent( previousBackground );
3614
// Start content in the current background
3615
if( currentBackground ) {
3617
startEmbeddedContent( currentBackground );
3619
var currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
3620
if( currentBackgroundContent ) {
3622
var backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
3624
// Restart GIFs (doesn't work in Firefox)
3625
if( /\.gif/i.test( backgroundImageURL ) ) {
3626
currentBackgroundContent.style.backgroundImage = '';
3627
window.getComputedStyle( currentBackgroundContent ).opacity;
3628
currentBackgroundContent.style.backgroundImage = backgroundImageURL;
3633
// Don't transition between identical backgrounds. This
3634
// prevents unwanted flicker.
3635
var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
3636
var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
3637
if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
3638
dom.background.classList.add( 'no-transition' );
3641
previousBackground = currentBackground;
3645
// If there's a background brightness flag for this slide,
3646
// bubble it to the .reveal container
3647
if( currentSlide ) {
3648
[ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
3649
if( currentSlide.classList.contains( classToBubble ) ) {
3650
dom.wrapper.classList.add( classToBubble );
3653
dom.wrapper.classList.remove( classToBubble );
3658
// Allow the first background to apply without transition
3659
setTimeout( function() {
3660
dom.background.classList.remove( 'no-transition' );
3666
* Updates the position of the parallax background based
3667
* on the current slide index.
3669
function updateParallax() {
3671
if( config.parallaxBackgroundImage ) {
3673
var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
3674
verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
3676
var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
3677
backgroundWidth, backgroundHeight;
3679
if( backgroundSize.length === 1 ) {
3680
backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
3683
backgroundWidth = parseInt( backgroundSize[0], 10 );
3684
backgroundHeight = parseInt( backgroundSize[1], 10 );
3687
var slideWidth = dom.background.offsetWidth,
3688
horizontalSlideCount = horizontalSlides.length,
3689
horizontalOffsetMultiplier,
3692
if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
3693
horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
3696
horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
3699
horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
3701
var slideHeight = dom.background.offsetHeight,
3702
verticalSlideCount = verticalSlides.length,
3703
verticalOffsetMultiplier,
3706
if( typeof config.parallaxBackgroundVertical === 'number' ) {
3707
verticalOffsetMultiplier = config.parallaxBackgroundVertical;
3710
verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
3713
verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0;
3715
dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
3722
* Should the given element be preloaded?
3723
* Decides based on local element attributes and global config.
3725
* @param {HTMLElement} element
3727
function shouldPreload( element ) {
3729
// Prefer an explicit global preload setting
3730
var preload = config.preloadIframes;
3732
// If no global setting is available, fall back on the element's
3733
// own preload setting
3734
if( typeof preload !== 'boolean' ) {
3735
preload = element.hasAttribute( 'data-preload' );
3742
* Called when the given slide is within the configured view
3743
* distance. Shows the slide element and loads any content
3744
* that is set to load lazily (data-src).
3746
* @param {HTMLElement} slide Slide to show
3748
function loadSlide( slide, options ) {
3750
options = options || {};
3752
// Show the slide element
3753
slide.style.display = config.display;
3755
// Media elements with data-src attributes
3756
toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
3757
if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
3758
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
3759
element.setAttribute( 'data-lazy-loaded', '' );
3760
element.removeAttribute( 'data-src' );
3764
// Media elements with <source> children
3765
toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
3768
toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
3769
source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
3770
source.removeAttribute( 'data-src' );
3771
source.setAttribute( 'data-lazy-loaded', '' );
3775
// If we rewrote sources for this video/audio element, we need
3776
// to manually tell it to load from its new origin
3783
// Show the corresponding background element
3784
var background = slide.slideBackgroundElement;
3786
background.style.display = 'block';
3788
var backgroundContent = slide.slideBackgroundContentElement;
3790
// If the background contains media, load it
3791
if( background.hasAttribute( 'data-loaded' ) === false ) {
3792
background.setAttribute( 'data-loaded', 'true' );
3794
var backgroundImage = slide.getAttribute( 'data-background-image' ),
3795
backgroundVideo = slide.getAttribute( 'data-background-video' ),
3796
backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
3797
backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
3798
backgroundIframe = slide.getAttribute( 'data-background-iframe' );
3801
if( backgroundImage ) {
3802
backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')';
3805
else if ( backgroundVideo && !isSpeakerNotes() ) {
3806
var video = document.createElement( 'video' );
3808
if( backgroundVideoLoop ) {
3809
video.setAttribute( 'loop', '' );
3812
if( backgroundVideoMuted ) {
3816
// Inline video playback works (at least in Mobile Safari) as
3817
// long as the video is muted and the `playsinline` attribute is
3819
if( isMobileDevice ) {
3821
video.autoplay = true;
3822
video.setAttribute( 'playsinline', '' );
3825
// Support comma separated lists of video sources
3826
backgroundVideo.split( ',' ).forEach( function( source ) {
3827
video.innerHTML += '<source src="'+ source +'">';
3830
backgroundContent.appendChild( video );
3833
else if( backgroundIframe && options.excludeIframes !== true ) {
3834
var iframe = document.createElement( 'iframe' );
3835
iframe.setAttribute( 'allowfullscreen', '' );
3836
iframe.setAttribute( 'mozallowfullscreen', '' );
3837
iframe.setAttribute( 'webkitallowfullscreen', '' );
3839
// Only load autoplaying content when the slide is shown to
3840
// avoid having it play in the background
3841
if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
3842
iframe.setAttribute( 'data-src', backgroundIframe );
3845
iframe.setAttribute( 'src', backgroundIframe );
3848
iframe.style.width = '100%';
3849
iframe.style.height = '100%';
3850
iframe.style.maxHeight = '100%';
3851
iframe.style.maxWidth = '100%';
3853
backgroundContent.appendChild( iframe );
3862
* Unloads and hides the given slide. This is called when the
3863
* slide is moved outside of the configured view distance.
3865
* @param {HTMLElement} slide
3867
function unloadSlide( slide ) {
3869
// Hide the slide element
3870
slide.style.display = 'none';
3872
// Hide the corresponding background element
3873
var background = getSlideBackground( slide );
3875
background.style.display = 'none';
3878
// Reset lazy-loaded media elements with src attributes
3879
toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
3880
element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
3881
element.removeAttribute( 'src' );
3884
// Reset lazy-loaded media elements with <source> children
3885
toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) {
3886
source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
3887
source.removeAttribute( 'src' );
3893
* Determine what available routes there are for navigation.
3895
* @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
3897
function availableRoutes() {
3899
var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
3900
verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
3904
right: indexh < horizontalSlides.length - 1,
3906
down: indexv < verticalSlides.length - 1
3909
// Looped presentations can always be navigated as long as
3910
// there are slides available
3912
if( horizontalSlides.length > 1 ) {
3914
routes.right = true;
3917
if( verticalSlides.length > 1 ) {
3923
// Reverse horizontal controls for rtl
3925
var left = routes.left;
3926
routes.left = routes.right;
3927
routes.right = left;
3935
* Returns an object describing the available fragment
3938
* @return {{prev: boolean, next: boolean}}
3940
function availableFragments() {
3942
if( currentSlide && config.fragments ) {
3943
var fragments = currentSlide.querySelectorAll( '.fragment' );
3944
var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
3947
prev: fragments.length - hiddenFragments.length > 0,
3948
next: !!hiddenFragments.length
3952
return { prev: false, next: false };
3958
* Enforces origin-specific format rules for embedded media.
3960
function formatEmbeddedContent() {
3962
var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
3963
toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
3964
var src = el.getAttribute( sourceAttribute );
3965
if( src && src.indexOf( param ) === -1 ) {
3966
el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
3971
// YouTube frames must include "?enablejsapi=1"
3972
_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
3973
_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
3975
// Vimeo frames must include "?api=1"
3976
_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
3977
_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
3982
* Start playback of any embedded content inside of
3983
* the given element.
3985
* @param {HTMLElement} element
3987
function startEmbeddedContent( element ) {
3989
if( element && !isSpeakerNotes() ) {
3992
toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
3993
// Setting the same unchanged source like this was confirmed
3994
// to work in Chrome, FF & Safari
3995
el.setAttribute( 'src', el.getAttribute( 'src' ) );
3998
// HTML5 media elements
3999
toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
4000
if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
4004
// Prefer an explicit global autoplay setting
4005
var autoplay = config.autoPlayMedia;
4007
// If no global setting is available, fall back on the element's
4008
// own autoplay setting
4009
if( typeof autoplay !== 'boolean' ) {
4010
autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
4013
if( autoplay && typeof el.play === 'function' ) {
4015
// If the media is ready, start playback
4016
if( el.readyState > 1 ) {
4017
startEmbeddedMedia( { target: el } );
4019
// Mobile devices never fire a loaded event so instead
4020
// of waiting, we initiate playback
4021
else if( isMobileDevice ) {
4022
var promise = el.play();
4024
// If autoplay does not work, ensure that the controls are visible so
4025
// that the viewer can start the media on their own
4026
if( promise && typeof promise.catch === 'function' && el.controls === false ) {
4027
promise.catch( function() {
4030
// Once the video does start playing, hide the controls again
4031
el.addEventListener( 'play', function() {
4032
el.controls = false;
4037
// If the media isn't loaded, wait before playing
4039
el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
4040
el.addEventListener( 'loadeddata', startEmbeddedMedia );
4047
toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
4048
if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
4052
startEmbeddedIframe( { target: el } );
4055
// Lazy loading iframes
4056
toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
4057
if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
4061
if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
4062
el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
4063
el.addEventListener( 'load', startEmbeddedIframe );
4064
el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
4073
* Starts playing an embedded video/audio element after
4074
* it has finished loading.
4076
* @param {object} event
4078
function startEmbeddedMedia( event ) {
4080
var isAttachedToDOM = !!closestParent( event.target, 'html' ),
4081
isVisible = !!closestParent( event.target, '.present' );
4083
if( isAttachedToDOM && isVisible ) {
4084
event.target.currentTime = 0;
4085
event.target.play();
4088
event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
4093
* "Starts" the content of an embedded iframe using the
4096
* @param {object} event
4098
function startEmbeddedIframe( event ) {
4100
var iframe = event.target;
4102
if( iframe && iframe.contentWindow ) {
4104
var isAttachedToDOM = !!closestParent( event.target, 'html' ),
4105
isVisible = !!closestParent( event.target, '.present' );
4107
if( isAttachedToDOM && isVisible ) {
4109
// Prefer an explicit global autoplay setting
4110
var autoplay = config.autoPlayMedia;
4112
// If no global setting is available, fall back on the element's
4113
// own autoplay setting
4114
if( typeof autoplay !== 'boolean' ) {
4115
autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
4118
// YouTube postMessage API
4119
if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
4120
iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
4122
// Vimeo postMessage API
4123
else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
4124
iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
4126
// Generic postMessage API
4128
iframe.contentWindow.postMessage( 'slide:start', '*' );
4138
* Stop playback of any embedded content inside of
4139
* the targeted slide.
4141
* @param {HTMLElement} element
4143
function stopEmbeddedContent( element, options ) {
4150
if( element && element.parentNode ) {
4151
// HTML5 media elements
4152
toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
4153
if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
4154
el.setAttribute('data-paused-by-reveal', '');
4159
// Generic postMessage API for non-lazy loaded iframes
4160
toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
4161
if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
4162
el.removeEventListener( 'load', startEmbeddedIframe );
4165
// YouTube postMessage API
4166
toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
4167
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
4168
el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
4172
// Vimeo postMessage API
4173
toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
4174
if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
4175
el.contentWindow.postMessage( '{"method":"pause"}', '*' );
4179
if( options.unloadIframes === true ) {
4180
// Unload lazy-loaded iframes
4181
toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
4182
// Only removing the src doesn't actually unload the frame
4183
// in all browsers (Firefox) so we set it to blank first
4184
el.setAttribute( 'src', 'about:blank' );
4185
el.removeAttribute( 'src' );
4193
* Returns the number of past slides. This can be used as a global
4194
* flattened index for slides.
4196
* @return {number} Past slide count
4198
function getSlidePastCount() {
4200
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
4202
// The number of past slides
4205
// Step through all slides and count the past ones
4206
mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
4208
var horizontalSlide = horizontalSlides[i];
4209
var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
4211
for( var j = 0; j < verticalSlides.length; j++ ) {
4213
// Stop as soon as we arrive at the present
4214
if( verticalSlides[j].classList.contains( 'present' ) ) {
4222
// Stop as soon as we arrive at the present
4223
if( horizontalSlide.classList.contains( 'present' ) ) {
4227
// Don't count the wrapping section for vertical slides
4228
if( horizontalSlide.classList.contains( 'stack' ) === false ) {
4239
* Returns a value ranging from 0-1 that represents
4240
* how far into the presentation we have navigated.
4244
function getProgress() {
4246
// The number of past and total slides
4247
var totalCount = getTotalSlides();
4248
var pastCount = getSlidePastCount();
4250
if( currentSlide ) {
4252
var allFragments = currentSlide.querySelectorAll( '.fragment' );
4254
// If there are fragments in the current slide those should be
4255
// accounted for in the progress.
4256
if( allFragments.length > 0 ) {
4257
var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
4259
// This value represents how big a portion of the slide progress
4260
// that is made up by its fragments (0-1)
4261
var fragmentWeight = 0.9;
4263
// Add fragment progress to the past slide count
4264
pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
4269
return Math.min( pastCount / ( totalCount - 1 ), 1 );
4274
* Checks if this presentation is running inside of the
4275
* speaker notes window.
4279
function isSpeakerNotes() {
4281
return !!window.location.search.match( /receiver/gi );
4286
* Reads the current URL (hash) and navigates accordingly.
4288
function readURL() {
4290
var hash = window.location.hash;
4292
// Attempt to parse the hash as either an index or name
4293
var bits = hash.slice( 2 ).split( '/' ),
4294
name = hash.replace( /#|\//gi, '' );
4296
// If the first bit is not fully numeric and there is a name we
4297
// can assume that this is a named link
4298
if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
4301
// Ensure the named link is a valid HTML ID attribute
4303
element = document.getElementById( decodeURIComponent( name ) );
4307
// Ensure that we're not already on a slide with the same name
4308
var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
4311
// If the slide exists and is not the current slide...
4312
if ( !isSameNameAsCurrentSlide ) {
4313
// ...find the position of the named slide and navigate to it
4314
var indices = Reveal.getIndices(element);
4315
slide(indices.h, indices.v);
4318
// If the slide doesn't exist, navigate to the current slide
4320
slide( indexh || 0, indexv || 0 );
4324
var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
4326
// Read the index components of the hash
4327
var h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
4328
v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
4331
if( config.fragmentInURL ) {
4332
f = parseInt( bits[2], 10 );
4338
if( h !== indexh || v !== indexv || f !== undefined ) {
4346
* Updates the page URL (hash) to reflect the current
4349
* @param {number} delay The time in ms to wait before
4352
function writeURL( delay ) {
4354
// Make sure there's never more than one timeout running
4355
clearTimeout( writeURLTimeout );
4357
// If a delay is specified, timeout this call
4358
if( typeof delay === 'number' ) {
4359
writeURLTimeout = setTimeout( writeURL, delay );
4361
else if( currentSlide ) {
4362
// If we're configured to push to history OR the history
4363
// API is not avaialble.
4364
if( config.history || !window.history ) {
4365
window.location.hash = locationHash();
4367
// If we're configured to reflect the current slide in the
4368
// URL without pushing to history.
4369
else if( config.hash ) {
4370
window.history.replaceState( null, null, '#' + locationHash() );
4372
// If history and hash are both disabled, a hash may still
4373
// be added to the URL by clicking on a href with a hash
4374
// target. Counter this by always removing the hash.
4376
window.history.replaceState( null, null, window.location.pathname + window.location.search );
4382
* Retrieves the h/v location and fragment of the current,
4383
* or specified, slide.
4385
* @param {HTMLElement} [slide] If specified, the returned
4386
* index will be for this slide rather than the currently
4389
* @return {{h: number, v: number, f: number}}
4391
function getIndices( slide ) {
4393
// By default, return the current indices
4398
// If a slide is specified, return the indices of that slide
4400
var isVertical = isVerticalSlide( slide );
4401
var slideh = isVertical ? slide.parentNode : slide;
4403
// Select all horizontal slides
4404
var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
4406
// Now that we know which the horizontal slide is, get its index
4407
h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
4409
// Assume we're not vertical
4412
// If this is a vertical slide, grab the vertical index
4414
v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
4418
if( !slide && currentSlide ) {
4419
var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
4420
if( hasFragments ) {
4421
var currentFragment = currentSlide.querySelector( '.current-fragment' );
4422
if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
4423
f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
4426
f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
4431
return { h: h, v: v, f: f };
4436
* Retrieves all slides in this presentation.
4438
function getSlides() {
4440
return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
4445
* Returns an array of objects where each object represents the
4446
* attributes on its respective slide.
4448
function getSlidesAttributes() {
4450
return getSlides().map( function( slide ) {
4452
var attributes = {};
4453
for( var i = 0; i < slide.attributes.length; i++ ) {
4454
var attribute = slide.attributes[ i ];
4455
attributes[ attribute.name ] = attribute.value;
4464
* Retrieves the total number of slides in this presentation.
4468
function getTotalSlides() {
4470
return getSlides().length;
4475
* Returns the slide element matching the specified index.
4477
* @return {HTMLElement}
4479
function getSlide( x, y ) {
4481
var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
4482
var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
4484
if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
4485
return verticalSlides ? verticalSlides[ y ] : undefined;
4488
return horizontalSlide;
4493
* Returns the background element for the given slide.
4494
* All slides, even the ones with no background properties
4495
* defined, have a background element so as long as the
4496
* index is valid an element will be returned.
4498
* @param {mixed} x Horizontal background index OR a slide
4500
* @param {number} y Vertical background index
4501
* @return {(HTMLElement[]|*)}
4503
function getSlideBackground( x, y ) {
4505
var slide = typeof x === 'number' ? getSlide( x, y ) : x;
4507
return slide.slideBackgroundElement;
4515
* Retrieves the speaker notes from a slide. Notes can be
4516
* defined in two ways:
4517
* 1. As a data-notes attribute on the slide <section>
4518
* 2. As an <aside class="notes"> inside of the slide
4520
* @param {HTMLElement} [slide=currentSlide]
4521
* @return {(string|null)}
4523
function getSlideNotes( slide ) {
4525
// Default to the current slide
4526
slide = slide || currentSlide;
4528
// Notes can be specified via the data-notes attribute...
4529
if( slide.hasAttribute( 'data-notes' ) ) {
4530
return slide.getAttribute( 'data-notes' );
4533
// ... or using an <aside class="notes"> element
4534
var notesElement = slide.querySelector( 'aside.notes' );
4535
if( notesElement ) {
4536
return notesElement.innerHTML;
4544
* Retrieves the current state of the presentation as
4545
* an object. This state can then be restored at any
4548
* @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
4550
function getState() {
4552
var indices = getIndices();
4559
overview: isOverview()
4565
* Restores the presentation to the given state.
4567
* @param {object} state As generated by getState()
4568
* @see {@link getState} generates the parameter `state`
4570
function setState( state ) {
4572
if( typeof state === 'object' ) {
4573
slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
4575
var pausedFlag = deserialize( state.paused ),
4576
overviewFlag = deserialize( state.overview );
4578
if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
4579
togglePause( pausedFlag );
4582
if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
4583
toggleOverview( overviewFlag );
4590
* Return a sorted fragments list, ordered by an increasing
4591
* "data-fragment-index" attribute.
4593
* Fragments will be revealed in the order that they are returned by
4594
* this function, so you can use the index attributes to control the
4595
* order of fragment appearance.
4597
* To maintain a sensible default fragment order, fragments are presumed
4598
* to be passed in document order. This function adds a "fragment-index"
4599
* attribute to each node if such an attribute is not already present,
4600
* and sets that attribute to an integer value which is the position of
4601
* the fragment within the fragments list.
4603
* @param {object[]|*} fragments
4604
* @param {boolean} grouped If true the returned array will contain
4605
* nested arrays for all fragments with the same index
4606
* @return {object[]} sorted Sorted array of fragments
4608
function sortFragments( fragments, grouped ) {
4610
fragments = toArray( fragments );
4616
// Group ordered and unordered elements
4617
fragments.forEach( function( fragment, i ) {
4618
if( fragment.hasAttribute( 'data-fragment-index' ) ) {
4619
var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
4621
if( !ordered[index] ) {
4622
ordered[index] = [];
4625
ordered[index].push( fragment );
4628
unordered.push( [ fragment ] );
4632
// Append fragments without explicit indices in their
4634
ordered = ordered.concat( unordered );
4636
// Manually count the index up per group to ensure there
4640
// Push all fragments in their sorted order to an array,
4641
// this flattens the groups
4642
ordered.forEach( function( group ) {
4643
group.forEach( function( fragment ) {
4644
sorted.push( fragment );
4645
fragment.setAttribute( 'data-fragment-index', index );
4651
return grouped === true ? ordered : sorted;
4656
* Refreshes the fragments on the current slide so that they
4657
* have the appropriate classes (.visible + .current-fragment).
4659
* @param {number} [index] The index of the current fragment
4660
* @param {array} [fragments] Array containing all fragments
4661
* in the current slide
4663
* @return {{shown: array, hidden: array}}
4665
function updateFragments( index, fragments ) {
4667
var changedFragments = {
4672
if( currentSlide && config.fragments ) {
4674
fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
4676
if( fragments.length ) {
4678
if( typeof index !== 'number' ) {
4679
var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
4680
if( currentFragment ) {
4681
index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
4685
toArray( fragments ).forEach( function( el, i ) {
4687
if( el.hasAttribute( 'data-fragment-index' ) ) {
4688
i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
4691
// Visible fragments
4693
if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
4694
el.classList.add( 'visible' );
4695
el.classList.remove( 'current-fragment' );
4697
// Announce the fragments one by one to the Screen Reader
4698
dom.statusDiv.textContent = getStatusText( el );
4701
el.classList.add( 'current-fragment' );
4702
startEmbeddedContent( el );
4707
if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
4708
el.classList.remove( 'visible' );
4709
el.classList.remove( 'current-fragment' );
4718
return changedFragments;
4723
* Navigate to the specified slide fragment.
4725
* @param {?number} index The index of the fragment that
4726
* should be shown, -1 means all are invisible
4727
* @param {number} offset Integer offset to apply to the
4730
* @return {boolean} true if a change was made in any
4731
* fragments visibility as part of this call
4733
function navigateFragment( index, offset ) {
4735
if( currentSlide && config.fragments ) {
4737
var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
4738
if( fragments.length ) {
4740
// If no index is specified, find the current
4741
if( typeof index !== 'number' ) {
4742
var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
4744
if( lastVisibleFragment ) {
4745
index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
4752
// If an offset is specified, apply it to the index
4753
if( typeof offset === 'number' ) {
4757
var changedFragments = updateFragments( index, fragments );
4759
if( changedFragments.hidden.length ) {
4760
dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
4763
if( changedFragments.shown.length ) {
4764
dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
4770
if( config.fragmentInURL ) {
4774
return !!( changedFragments.shown.length || changedFragments.hidden.length );
4785
* Navigate to the next slide fragment.
4787
* @return {boolean} true if there was a next fragment,
4790
function nextFragment() {
4792
return navigateFragment( null, 1 );
4797
* Navigate to the previous slide fragment.
4799
* @return {boolean} true if there was a previous fragment,
4802
function previousFragment() {
4804
return navigateFragment( null, -1 );
4809
* Cues a new automated slide if enabled in the config.
4811
function cueAutoSlide() {
4815
if( currentSlide && config.autoSlide !== false ) {
4817
var fragment = currentSlide.querySelector( '.current-fragment' );
4819
// When the slide first appears there is no "current" fragment so
4820
// we look for a data-autoslide timing on the first fragment
4821
if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
4823
var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
4824
var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
4825
var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
4827
// Pick value in the following priority order:
4828
// 1. Current fragment's data-autoslide
4829
// 2. Current slide's data-autoslide
4830
// 3. Parent slide's data-autoslide
4831
// 4. Global autoSlide setting
4832
if( fragmentAutoSlide ) {
4833
autoSlide = parseInt( fragmentAutoSlide, 10 );
4835
else if( slideAutoSlide ) {
4836
autoSlide = parseInt( slideAutoSlide, 10 );
4838
else if( parentAutoSlide ) {
4839
autoSlide = parseInt( parentAutoSlide, 10 );
4842
autoSlide = config.autoSlide;
4845
// If there are media elements with data-autoplay,
4846
// automatically set the autoSlide duration to the
4847
// length of that media. Not applicable if the slide
4848
// is divided up into fragments.
4849
// playbackRate is accounted for in the duration.
4850
if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
4851
toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
4852
if( el.hasAttribute( 'data-autoplay' ) ) {
4853
if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
4854
autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
4860
// Cue the next auto-slide if:
4861
// - There is an autoSlide value
4862
// - Auto-sliding isn't paused by the user
4863
// - The presentation isn't paused
4864
// - The overview isn't active
4865
// - The presentation isn't over
4866
if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
4867
autoSlideTimeout = setTimeout( function() {
4868
typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
4871
autoSlideStartTime = Date.now();
4874
if( autoSlidePlayer ) {
4875
autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
4883
* Cancels any ongoing request to auto-slide.
4885
function cancelAutoSlide() {
4887
clearTimeout( autoSlideTimeout );
4888
autoSlideTimeout = -1;
4892
function pauseAutoSlide() {
4894
if( autoSlide && !autoSlidePaused ) {
4895
autoSlidePaused = true;
4896
dispatchEvent( 'autoslidepaused' );
4897
clearTimeout( autoSlideTimeout );
4899
if( autoSlidePlayer ) {
4900
autoSlidePlayer.setPlaying( false );
4906
function resumeAutoSlide() {
4908
if( autoSlide && autoSlidePaused ) {
4909
autoSlidePaused = false;
4910
dispatchEvent( 'autoslideresumed' );
4916
function navigateLeft() {
4920
if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
4921
slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
4924
// Normal navigation
4925
else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
4926
slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
4931
function navigateRight() {
4933
hasNavigatedRight = true;
4937
if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
4938
slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
4941
// Normal navigation
4942
else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
4943
slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
4948
function navigateUp() {
4950
// Prioritize hiding fragments
4951
if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
4952
slide( indexh, indexv - 1 );
4957
function navigateDown() {
4959
hasNavigatedDown = true;
4961
// Prioritize revealing fragments
4962
if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
4963
slide( indexh, indexv + 1 );
4969
* Navigates backwards, prioritized in the following order:
4970
* 1) Previous fragment
4971
* 2) Previous vertical slide
4972
* 3) Previous horizontal slide
4974
function navigatePrev() {
4976
// Prioritize revealing fragments
4977
if( previousFragment() === false ) {
4978
if( availableRoutes().up ) {
4982
// Fetch the previous horizontal slide, if there is one
4986
previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
4989
previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
4992
if( previousSlide ) {
4993
var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
5003
* The reverse of #navigatePrev().
5005
function navigateNext() {
5007
hasNavigatedRight = true;
5008
hasNavigatedDown = true;
5010
// Prioritize revealing fragments
5011
if( nextFragment() === false ) {
5013
var routes = availableRoutes();
5015
// When looping is enabled `routes.down` is always available
5016
// so we need a separate check for when we've reached the
5017
// end of a stack and should move horizontally
5018
if( routes.down && routes.right && config.loop && Reveal.isLastVerticalSlide( currentSlide ) ) {
5019
routes.down = false;
5025
else if( config.rtl ) {
5036
* Checks if the target element prevents the triggering of
5039
function isSwipePrevented( target ) {
5041
while( target && typeof target.hasAttribute === 'function' ) {
5042
if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
5043
target = target.parentNode;
5051
// --------------------------------------------------------------------//
5052
// ----------------------------- EVENTS -------------------------------//
5053
// --------------------------------------------------------------------//
5056
* Called by all event handlers that are based on user
5059
* @param {object} [event]
5061
function onUserInput( event ) {
5063
if( config.autoSlideStoppable ) {
5070
* Called whenever there is mouse input at the document level
5071
* to determine if the cursor is active or not.
5073
* @param {object} event
5075
function onDocumentCursorActive( event ) {
5079
clearTimeout( cursorInactiveTimeout );
5081
cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
5086
* Handler for the document level 'keypress' event.
5088
* @param {object} event
5090
function onDocumentKeyPress( event ) {
5092
// Check if the pressed key is question mark
5093
if( event.shiftKey && event.charCode === 63 ) {
5100
* Handler for the document level 'keydown' event.
5102
* @param {object} event
5104
function onDocumentKeyDown( event ) {
5106
// If there's a condition specified and it returns false,
5107
// ignore this event
5108
if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
5113
var keyCode = event.keyCode;
5115
// Remember if auto-sliding was paused so we can toggle it
5116
var autoSlideWasPaused = autoSlidePaused;
5118
onUserInput( event );
5120
// Is there a focused element that could be using the keyboard?
5121
var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
5122
var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
5123
var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
5125
// Whitelist specific modified + keycode combinations
5126
var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
5127
var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
5128
var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
5130
// Prevent all other events when a modifier is pressed
5131
var unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
5132
( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
5134
// Disregard the event if there's a focused element or a
5135
// keyboard modifier key is present
5136
if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
5138
// While paused only allow resume keyboard events; 'b', 'v', '.'
5139
var resumeKeyCodes = [66,86,190,191];
5142
// Custom key bindings for togglePause should be able to resume
5143
if( typeof config.keyboard === 'object' ) {
5144
for( key in config.keyboard ) {
5145
if( config.keyboard[key] === 'togglePause' ) {
5146
resumeKeyCodes.push( parseInt( key, 10 ) );
5151
if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
5155
var triggered = false;
5157
// 1. User defined key bindings
5158
if( typeof config.keyboard === 'object' ) {
5160
for( key in config.keyboard ) {
5162
// Check if this binding matches the pressed key
5163
if( parseInt( key, 10 ) === keyCode ) {
5165
var value = config.keyboard[ key ];
5167
// Callback function
5168
if( typeof value === 'function' ) {
5169
value.apply( null, [ event ] );
5171
// String shortcuts to reveal.js API
5172
else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
5173
Reveal[ value ].call();
5184
// 2. Registered custom key bindings
5185
if( triggered === false ) {
5187
for( key in registeredKeyBindings ) {
5189
// Check if this binding matches the pressed key
5190
if( parseInt( key, 10 ) === keyCode ) {
5192
var action = registeredKeyBindings[ key ].callback;
5194
// Callback function
5195
if( typeof action === 'function' ) {
5196
action.apply( null, [ event ] );
5198
// String shortcuts to reveal.js API
5199
else if( typeof action === 'string' && typeof Reveal[ action ] === 'function' ) {
5200
Reveal[ action ].call();
5208
// 3. System defined key bindings
5209
if( triggered === false ) {
5211
// Assume true and try to prove false
5215
if( keyCode === 80 || keyCode === 33 ) {
5219
else if( keyCode === 78 || keyCode === 34 ) {
5223
else if( keyCode === 72 || keyCode === 37 ) {
5224
if( firstSlideShortcut ) {
5227
else if( !isOverview() && config.navigationMode === 'linear' ) {
5235
else if( keyCode === 76 || keyCode === 39 ) {
5236
if( lastSlideShortcut ) {
5237
slide( Number.MAX_VALUE );
5239
else if( !isOverview() && config.navigationMode === 'linear' ) {
5247
else if( keyCode === 75 || keyCode === 38 ) {
5248
if( !isOverview() && config.navigationMode === 'linear' ) {
5256
else if( keyCode === 74 || keyCode === 40 ) {
5257
if( !isOverview() && config.navigationMode === 'linear' ) {
5265
else if( keyCode === 36 ) {
5269
else if( keyCode === 35 ) {
5270
slide( Number.MAX_VALUE );
5273
else if( keyCode === 32 ) {
5274
if( isOverview() ) {
5275
deactivateOverview();
5277
if( event.shiftKey ) {
5284
// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
5285
else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
5289
else if( keyCode === 70 ) {
5293
else if( keyCode === 65 ) {
5294
if ( config.autoSlideStoppable ) {
5295
toggleAutoSlide( autoSlideWasPaused );
5304
// If the input resulted in a triggered action we should prevent
5305
// the browsers default behavior
5307
event.preventDefault && event.preventDefault();
5310
else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
5318
event.preventDefault && event.preventDefault();
5321
// If auto-sliding is enabled we need to cue up
5328
* Handler for the 'touchstart' event, enables support for
5329
* swipe and pinch gestures.
5331
* @param {object} event
5333
function onTouchStart( event ) {
5335
if( isSwipePrevented( event.target ) ) return true;
5337
touch.startX = event.touches[0].clientX;
5338
touch.startY = event.touches[0].clientY;
5339
touch.startCount = event.touches.length;
5344
* Handler for the 'touchmove' event.
5346
* @param {object} event
5348
function onTouchMove( event ) {
5350
if( isSwipePrevented( event.target ) ) return true;
5352
// Each touch should only trigger one action
5353
if( !touch.captured ) {
5354
onUserInput( event );
5356
var currentX = event.touches[0].clientX;
5357
var currentY = event.touches[0].clientY;
5359
// There was only one touch point, look for a swipe
5360
if( event.touches.length === 1 && touch.startCount !== 2 ) {
5362
var deltaX = currentX - touch.startX,
5363
deltaY = currentY - touch.startY;
5365
if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
5366
touch.captured = true;
5369
else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
5370
touch.captured = true;
5373
else if( deltaY > touch.threshold ) {
5374
touch.captured = true;
5377
else if( deltaY < -touch.threshold ) {
5378
touch.captured = true;
5382
// If we're embedded, only block touch events if they have
5383
// triggered an action
5384
if( config.embedded ) {
5385
if( touch.captured || isVerticalSlide( currentSlide ) ) {
5386
event.preventDefault();
5389
// Not embedded? Block them all to avoid needless tossing
5390
// around of the viewport in iOS
5392
event.preventDefault();
5397
// There's a bug with swiping on some Android devices unless
5398
// the default action is always prevented
5399
else if( UA.match( /android/gi ) ) {
5400
event.preventDefault();
5406
* Handler for the 'touchend' event.
5408
* @param {object} event
5410
function onTouchEnd( event ) {
5412
touch.captured = false;
5417
* Convert pointer down to touch start.
5419
* @param {object} event
5421
function onPointerDown( event ) {
5423
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
5424
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
5425
onTouchStart( event );
5431
* Convert pointer move to touch move.
5433
* @param {object} event
5435
function onPointerMove( event ) {
5437
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
5438
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
5439
onTouchMove( event );
5445
* Convert pointer up to touch end.
5447
* @param {object} event
5449
function onPointerUp( event ) {
5451
if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
5452
event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
5453
onTouchEnd( event );
5459
* Handles mouse wheel scrolling, throttled to avoid skipping
5462
* @param {object} event
5464
function onDocumentMouseScroll( event ) {
5466
if( Date.now() - lastMouseWheelStep > 600 ) {
5468
lastMouseWheelStep = Date.now();
5470
var delta = event.detail || -event.wheelDelta;
5474
else if( delta < 0 ) {
5483
* Clicking on the progress bar results in a navigation to the
5484
* closest approximate horizontal slide using this equation:
5486
* ( clickX / presentationWidth ) * numberOfSlides
5488
* @param {object} event
5490
function onProgressClicked( event ) {
5492
onUserInput( event );
5494
event.preventDefault();
5496
var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
5497
var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
5500
slideIndex = slidesTotal - slideIndex;
5503
slide( slideIndex );
5508
* Event handler for navigation control buttons.
5510
function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
5511
function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
5512
function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
5513
function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
5514
function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
5515
function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
5518
* Handler for the window level 'hashchange' event.
5520
* @param {object} [event]
5522
function onWindowHashChange( event ) {
5529
* Handler for the window level 'resize' event.
5531
* @param {object} [event]
5533
function onWindowResize( event ) {
5540
* Handle for the window level 'visibilitychange' event.
5542
* @param {object} [event]
5544
function onPageVisibilityChange( event ) {
5546
var isHidden = document.webkitHidden ||
5547
document.msHidden ||
5550
// If, after clicking a link or similar and we're coming back,
5551
// focus the document.body to ensure we can use keyboard shortcuts
5552
if( isHidden === false && document.activeElement !== document.body ) {
5553
// Not all elements support .blur() - SVGs among them.
5554
if( typeof document.activeElement.blur === 'function' ) {
5555
document.activeElement.blur();
5557
document.body.focus();
5563
* Invoked when a slide is and we're in the overview.
5565
* @param {object} event
5567
function onOverviewSlideClicked( event ) {
5569
// TODO There's a bug here where the event listeners are not
5570
// removed after deactivating the overview.
5571
if( eventsAreBound && isOverview() ) {
5572
event.preventDefault();
5574
var element = event.target;
5576
while( element && !element.nodeName.match( /section/gi ) ) {
5577
element = element.parentNode;
5580
if( element && !element.classList.contains( 'disabled' ) ) {
5582
deactivateOverview();
5584
if( element.nodeName.match( /section/gi ) ) {
5585
var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
5586
v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
5597
* Handles clicks on links that are set to preview in the
5600
* @param {object} event
5602
function onPreviewLinkClicked( event ) {
5604
if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
5605
var url = event.currentTarget.getAttribute( 'href' );
5608
event.preventDefault();
5615
* Handles click on the auto-sliding controls element.
5617
* @param {object} [event]
5619
function onAutoSlidePlayerClick( event ) {
5622
if( Reveal.isLastSlide() && config.loop === false ) {
5627
else if( autoSlidePaused ) {
5638
// --------------------------------------------------------------------//
5639
// ------------------------ PLAYBACK COMPONENT ------------------------//
5640
// --------------------------------------------------------------------//
5644
* Constructor for the playback component, which displays
5645
* play/pause/progress controls.
5647
* @param {HTMLElement} container The component will append
5649
* @param {function} progressCheck A method which will be
5650
* called frequently to get the current progress on a range
5653
function Playback( container, progressCheck ) {
5656
this.diameter = 100;
5657
this.diameter2 = this.diameter/2;
5660
// Flags if we are currently playing
5661
this.playing = false;
5663
// Current progress on a 0-1 range
5666
// Used to loop the animation smoothly
5667
this.progressOffset = 1;
5669
this.container = container;
5670
this.progressCheck = progressCheck;
5672
this.canvas = document.createElement( 'canvas' );
5673
this.canvas.className = 'playback';
5674
this.canvas.width = this.diameter;
5675
this.canvas.height = this.diameter;
5676
this.canvas.style.width = this.diameter2 + 'px';
5677
this.canvas.style.height = this.diameter2 + 'px';
5678
this.context = this.canvas.getContext( '2d' );
5680
this.container.appendChild( this.canvas );
5689
Playback.prototype.setPlaying = function( value ) {
5691
var wasPlaying = this.playing;
5693
this.playing = value;
5695
// Start repainting if we weren't already
5696
if( !wasPlaying && this.playing ) {
5705
Playback.prototype.animate = function() {
5707
var progressBefore = this.progress;
5709
this.progress = this.progressCheck();
5711
// When we loop, offset the progress so that it eases
5712
// smoothly rather than immediately resetting
5713
if( progressBefore > 0.8 && this.progress < 0.2 ) {
5714
this.progressOffset = this.progress;
5719
if( this.playing ) {
5720
features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
5726
* Renders the current progress and playback state.
5728
Playback.prototype.render = function() {
5730
var progress = this.playing ? this.progress : 0,
5731
radius = ( this.diameter2 ) - this.thickness,
5737
this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
5739
var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
5740
var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
5742
this.context.save();
5743
this.context.clearRect( 0, 0, this.diameter, this.diameter );
5745
// Solid background color
5746
this.context.beginPath();
5747
this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
5748
this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
5749
this.context.fill();
5751
// Draw progress track
5752
this.context.beginPath();
5753
this.context.arc( x, y, radius, 0, Math.PI * 2, false );
5754
this.context.lineWidth = this.thickness;
5755
this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
5756
this.context.stroke();
5758
if( this.playing ) {
5759
// Draw progress on top of track
5760
this.context.beginPath();
5761
this.context.arc( x, y, radius, startAngle, endAngle, false );
5762
this.context.lineWidth = this.thickness;
5763
this.context.strokeStyle = '#fff';
5764
this.context.stroke();
5767
this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
5769
// Draw play/pause icons
5770
if( this.playing ) {
5771
this.context.fillStyle = '#fff';
5772
this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
5773
this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
5776
this.context.beginPath();
5777
this.context.translate( 4, 0 );
5778
this.context.moveTo( 0, 0 );
5779
this.context.lineTo( iconSize - 4, iconSize / 2 );
5780
this.context.lineTo( 0, iconSize );
5781
this.context.fillStyle = '#fff';
5782
this.context.fill();
5785
this.context.restore();
5789
Playback.prototype.on = function( type, listener ) {
5790
this.canvas.addEventListener( type, listener, false );
5793
Playback.prototype.off = function( type, listener ) {
5794
this.canvas.removeEventListener( type, listener, false );
5797
Playback.prototype.destroy = function() {
5799
this.playing = false;
5801
if( this.canvas.parentNode ) {
5802
this.container.removeChild( this.canvas );
5808
// --------------------------------------------------------------------//
5809
// ------------------------------- API --------------------------------//
5810
// --------------------------------------------------------------------//
5816
initialize: initialize,
5817
reinitialize: reinitialize,
5818
configure: configure,
5821
syncSlide: syncSlide,
5822
syncFragments: syncFragments,
5824
// Navigation methods
5827
right: navigateRight,
5834
navigateFragment: navigateFragment,
5835
prevFragment: previousFragment,
5836
nextFragment: nextFragment,
5838
// Deprecated aliases
5840
navigateLeft: navigateLeft,
5841
navigateRight: navigateRight,
5842
navigateUp: navigateUp,
5843
navigateDown: navigateDown,
5844
navigatePrev: navigatePrev,
5845
navigateNext: navigateNext,
5847
// Forces an update in slide layout
5850
// Randomizes the order of slides
5853
// Returns an object with the available routes as booleans (left/right/top/bottom)
5854
availableRoutes: availableRoutes,
5856
// Returns an object with the available fragments as booleans (prev/next)
5857
availableFragments: availableFragments,
5859
// Toggles a help overlay with keyboard shortcuts
5860
toggleHelp: toggleHelp,
5862
// Toggles the overview mode on/off
5863
toggleOverview: toggleOverview,
5865
// Toggles the "black screen" mode on/off
5866
togglePause: togglePause,
5868
// Toggles the auto slide mode on/off
5869
toggleAutoSlide: toggleAutoSlide,
5872
isOverview: isOverview,
5874
isAutoSliding: isAutoSliding,
5875
isSpeakerNotes: isSpeakerNotes,
5878
loadSlide: loadSlide,
5879
unloadSlide: unloadSlide,
5881
// Adds or removes all internal event listeners (such as keyboard)
5882
addEventListeners: addEventListeners,
5883
removeEventListeners: removeEventListeners,
5885
// Facility for persisting and restoring the presentation state
5889
// Presentation progress
5890
getSlidePastCount: getSlidePastCount,
5892
// Presentation progress on range of 0-1
5893
getProgress: getProgress,
5895
// Returns the indices of the current, or specified, slide
5896
getIndices: getIndices,
5898
// Returns an Array of all slides
5899
getSlides: getSlides,
5901
// Returns an Array of objects representing the attributes on
5903
getSlidesAttributes: getSlidesAttributes,
5905
// Returns the total number of slides
5906
getTotalSlides: getTotalSlides,
5908
// Returns the slide element at the specified index
5911
// Returns the slide background element at the specified index
5912
getSlideBackground: getSlideBackground,
5914
// Returns the speaker notes string for a slide, or null
5915
getSlideNotes: getSlideNotes,
5917
// Returns the previous slide element, may be null
5918
getPreviousSlide: function() {
5919
return previousSlide;
5922
// Returns the current slide element
5923
getCurrentSlide: function() {
5924
return currentSlide;
5927
// Returns the current scale of the presentation content
5928
getScale: function() {
5932
// Returns the current configuration object
5933
getConfig: function() {
5937
// Helper method, retrieves query string as a key/value hash
5938
getQueryHash: function() {
5941
location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
5942
query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
5945
// Basic deserialization
5946
for( var i in query ) {
5947
var value = query[ i ];
5949
query[ i ] = deserialize( unescape( value ) );
5955
// Returns the top-level DOM element
5956
getRevealElement: function() {
5957
return dom.wrapper || document.querySelector( '.reveal' );
5960
// Returns a hash with all registered plugins
5961
getPlugins: function() {
5965
// Returns true if we're currently on the first slide
5966
isFirstSlide: function() {
5967
return ( indexh === 0 && indexv === 0 );
5970
// Returns true if we're currently on the last slide
5971
isLastSlide: function() {
5972
if( currentSlide ) {
5973
// Does this slide have a next sibling?
5974
if( currentSlide.nextElementSibling ) return false;
5976
// If it's vertical, does its parent have a next sibling?
5977
if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
5985
// Returns true if we're on the last slide in the current
5987
isLastVerticalSlide: function() {
5988
if( currentSlide && isVerticalSlide( currentSlide ) ) {
5989
// Does this slide have a next sibling?
5990
if( currentSlide.nextElementSibling ) return false;
5998
// Checks if reveal.js has been loaded and is ready for use
5999
isReady: function() {
6003
// Forward event binding to the reveal DOM element
6004
addEventListener: function( type, listener, useCapture ) {
6005
if( 'addEventListener' in window ) {
6006
Reveal.getRevealElement().addEventListener( type, listener, useCapture );
6009
removeEventListener: function( type, listener, useCapture ) {
6010
if( 'addEventListener' in window ) {
6011
Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
6015
// Adds/removes a custom key binding
6016
addKeyBinding: addKeyBinding,
6017
removeKeyBinding: removeKeyBinding,
6019
// API for registering and retrieving plugins
6020
registerPlugin: registerPlugin,
6021
hasPlugin: hasPlugin,
6022
getPlugin: getPlugin,
6024
// Programmatically triggers a keyboard event
6025
triggerKey: function( keyCode ) {
6026
onDocumentKeyDown( { keyCode: keyCode } );
6029
// Registers a new shortcut to include in the help overlay
6030
registerKeyboardShortcut: function( key, value ) {
6031
keyboardShortcuts[key] = value;