Merge lp:~tomasgroth/openlp/revealjs-380 into lp:openlp

Proposed by Tomas Groth on 2019-04-08
Status: Merged
Approved by: Raoul Snyman on 2019-04-09
Approved revision: 2854
Merged at revision: 2855
Proposed branch: lp:~tomasgroth/openlp/revealjs-380
Merge into: lp:openlp
Diff against target: 1569 lines (+761/-310)
1 file modified
openlp/core/display/html/reveal.js (+761/-310)
To merge this branch: bzr merge lp:~tomasgroth/openlp/revealjs-380
Reviewer Review Type Date Requested Status
OpenLP Core 2019-04-08 Pending
Review via email: mp+365698@code.launchpad.net

Commit message

Update Reveal.js to 3.8.0.

To post a comment you must log in.
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/display/html/reveal.js'
2--- openlp/core/display/html/reveal.js 2019-02-11 20:34:20 +0000
3+++ openlp/core/display/html/reveal.js 2019-04-08 19:57:45 +0000
4@@ -3,7 +3,7 @@
5 * http://revealjs.com
6 * MIT licensed
7 *
8- * Copyright (C) 2018 Hakim El Hattab, http://hakim.se
9+ * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
10 */
11 (function( root, factory ) {
12 if( typeof define === 'function' && define.amd ) {
13@@ -26,7 +26,7 @@
14 var Reveal;
15
16 // The reveal.js version
17- var VERSION = '3.7.0';
18+ var VERSION = '3.8.0';
19
20 var SLIDES_SELECTOR = '.slides section',
21 HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
22@@ -67,16 +67,36 @@
23 progress: true,
24
25 // Display the page number of the current slide
26+ // - true: Show slide number
27+ // - false: Hide slide number
28+ //
29+ // Can optionally be set as a string that specifies the number formatting:
30+ // - "h.v": Horizontal . vertical slide number (default)
31+ // - "h/v": Horizontal / vertical slide number
32+ // - "c": Flattened slide number
33+ // - "c/t": Flattened slide number / total slides
34+ //
35+ // Alternatively, you can provide a function that returns the slide
36+ // number for the current slide. The function needs to return an array
37+ // with one string [slideNumber] or three strings [n1,delimiter,n2].
38+ // See #formatSlideNumber().
39 slideNumber: false,
40
41+ // Can be used to limit the contexts in which the slide number appears
42+ // - "all": Always show the slide number
43+ // - "print": Only when printing to PDF
44+ // - "speaker": Only in the speaker view
45+ showSlideNumber: 'all',
46+
47 // Use 1 based indexing for # links to match slide number (default is zero
48 // based)
49 hashOneBasedIndex: false,
50
51- // Determine which displays to show the slide number on
52- showSlideNumber: 'all',
53+ // Add the current slide number to the URL hash so that reloading the
54+ // page/copying the URL will return you to the same slide
55+ hash: false,
56
57- // Push each slide change to the browser history
58+ // Push each slide change to the browser history. Implies `hash: true`
59 history: false,
60
61 // Enable keyboard shortcuts for navigation
62@@ -104,6 +124,32 @@
63 // Change the presentation direction to be RTL
64 rtl: false,
65
66+ // Changes the behavior of our navigation directions.
67+ //
68+ // "default"
69+ // Left/right arrow keys step between horizontal slides, up/down
70+ // arrow keys step between vertical slides. Space key steps through
71+ // all slides (both horizontal and vertical).
72+ //
73+ // "linear"
74+ // Removes the up/down arrows. Left/right arrows step through all
75+ // slides (both horizontal and vertical).
76+ //
77+ // "grid"
78+ // When this is enabled, stepping left/right from a vertical stack
79+ // to an adjacent vertical stack will land you at the same vertical
80+ // index.
81+ //
82+ // Consider a deck with six slides ordered in two vertical stacks:
83+ // 1.1 2.1
84+ // 1.2 2.2
85+ // 1.3 2.3
86+ //
87+ // If you're on slide 1.3 and navigate right, you will normally move
88+ // from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
89+ // from 1.3 -> 2.3.
90+ navigationMode: 'default',
91+
92 // Randomizes the order of slides each time the presentation loads
93 shuffle: false,
94
95@@ -134,6 +180,13 @@
96 // - false: No media will autoplay, regardless of individual setting
97 autoPlayMedia: null,
98
99+ // Global override for preloading lazy-loaded iframes
100+ // - null: Iframes with data-src AND data-preload will be loaded when within
101+ // the viewDistance, iframes with only data-src will be loaded when visible
102+ // - true: All iframes with data-src will be loaded when within the viewDistance
103+ // - false: All iframes with data-src will be loaded only when visible
104+ preloadIframes: null,
105+
106 // Controls automatic progression to the next slide
107 // - 0: Auto-sliding only happens if the data-autoslide HTML attribute
108 // is present on the current slide or fragment
109@@ -220,6 +273,12 @@
110 // The display mode that will be used to show slides
111 display: 'block',
112
113+ // Hide cursor if inactive
114+ hideInactiveCursor: true,
115+
116+ // Time before the cursor is hidden (in ms)
117+ hideCursorTime: 5000,
118+
119 // Script dependencies to load
120 dependencies: []
121
122@@ -267,6 +326,12 @@
123 // Cached references to DOM elements
124 dom = {},
125
126+ // A list of registered reveal.js plugins
127+ plugins = {},
128+
129+ // List of asynchronously loaded reveal.js dependencies
130+ asyncDependencies = [],
131+
132 // Features supported by the browser, see #checkCapabilities()
133 features = {},
134
135@@ -282,6 +347,12 @@
136 // Delays updates to the URL due to a Chrome thumbnailer bug
137 writeURLTimeout = 0,
138
139+ // Is the mouse pointer currently hidden from view
140+ cursorHidden = false,
141+
142+ // Timeout used to determine when the cursor is inactive
143+ cursorInactiveTimeout = 0,
144+
145 // Flags if the interaction event listeners are bound
146 eventsAreBound = false,
147
148@@ -298,26 +369,14 @@
149 touch = {
150 startX: 0,
151 startY: 0,
152- startSpan: 0,
153 startCount: 0,
154 captured: false,
155 threshold: 40
156 },
157
158- // Holds information about the keyboard shortcuts
159- keyboardShortcuts = {
160- 'N , SPACE': 'Next slide',
161- 'P': 'Previous slide',
162- '← , H': 'Navigate left',
163- '→ , L': 'Navigate right',
164- '↑ , K': 'Navigate up',
165- '↓ , J': 'Navigate down',
166- 'Home': 'First slide',
167- 'End': 'Last slide',
168- 'B , .': 'Pause',
169- 'F': 'Fullscreen',
170- 'ESC, O': 'Slide overview'
171- },
172+ // A key:value map of shortcut keyboard keys and descriptions of
173+ // the actions they trigger, generated in #configure()
174+ keyboardShortcuts = {},
175
176 // Holds custom key code mappings
177 registeredKeyBindings = {};
178@@ -377,7 +436,7 @@
179 // Hide the address bar in mobile browsers
180 hideAddressBar();
181
182- // Loads the dependencies and continues to #start() once done
183+ // Loads dependencies and continues to #start() once done
184 load();
185
186 }
187@@ -440,57 +499,148 @@
188 function load() {
189
190 var scripts = [],
191- scriptsAsync = [],
192- scriptsToPreload = 0;
193-
194- // Called once synchronous scripts finish loading
195- function proceed() {
196- if( scriptsAsync.length ) {
197- // Load asynchronous scripts
198- head.js.apply( null, scriptsAsync );
199- }
200-
201- start();
202- }
203-
204- function loadScript( s ) {
205- head.ready( s.src.match( /([\w\d_\-]*)\.?js(\?[\w\d.=&]*)?$|[^\\\/]*$/i )[0], function() {
206- // Extension may contain callback functions
207- if( typeof s.callback === 'function' ) {
208- s.callback.apply( this );
209- }
210-
211- if( --scriptsToPreload === 0 ) {
212- proceed();
213- }
214- });
215- }
216-
217- for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
218- var s = config.dependencies[i];
219-
220+ scriptsToLoad = 0;
221+
222+ config.dependencies.forEach( function( s ) {
223 // Load if there's no condition or the condition is truthy
224 if( !s.condition || s.condition() ) {
225 if( s.async ) {
226- scriptsAsync.push( s.src );
227+ asyncDependencies.push( s );
228 }
229 else {
230- scripts.push( s.src );
231+ scripts.push( s );
232 }
233-
234- loadScript( s );
235 }
236- }
237+ } );
238
239 if( scripts.length ) {
240- scriptsToPreload = scripts.length;
241+ scriptsToLoad = scripts.length;
242
243 // Load synchronous scripts
244- head.js.apply( null, scripts );
245- }
246- else {
247- proceed();
248- }
249+ scripts.forEach( function( s ) {
250+ loadScript( s.src, function() {
251+
252+ if( typeof s.callback === 'function' ) s.callback();
253+
254+ if( --scriptsToLoad === 0 ) {
255+ initPlugins();
256+ }
257+
258+ } );
259+ } );
260+ }
261+ else {
262+ initPlugins();
263+ }
264+
265+ }
266+
267+ /**
268+ * Initializes our plugins and waits for them to be ready
269+ * before proceeding.
270+ */
271+ function initPlugins() {
272+
273+ var pluginsToInitialize = Object.keys( plugins ).length;
274+
275+ // If there are no plugins, skip this step
276+ if( pluginsToInitialize === 0 ) {
277+ loadAsyncDependencies();
278+ }
279+ // ... otherwise initialize plugins
280+ else {
281+
282+ var afterPlugInitialized = function() {
283+ if( --pluginsToInitialize === 0 ) {
284+ loadAsyncDependencies();
285+ }
286+ };
287+
288+ for( var i in plugins ) {
289+
290+ var plugin = plugins[i];
291+
292+ // If the plugin has an 'init' method, invoke it
293+ if( typeof plugin.init === 'function' ) {
294+ var callback = plugin.init();
295+
296+ // If the plugin returned a Promise, wait for it
297+ if( callback && typeof callback.then === 'function' ) {
298+ callback.then( afterPlugInitialized );
299+ }
300+ else {
301+ afterPlugInitialized();
302+ }
303+ }
304+ else {
305+ afterPlugInitialized();
306+ }
307+
308+ }
309+
310+ }
311+
312+ }
313+
314+ /**
315+ * Loads all async reveal.js dependencies.
316+ */
317+ function loadAsyncDependencies() {
318+
319+ if( asyncDependencies.length ) {
320+ asyncDependencies.forEach( function( s ) {
321+ loadScript( s.src, s.callback );
322+ } );
323+ }
324+
325+ start();
326+
327+ }
328+
329+ /**
330+ * Loads a JavaScript file from the given URL and executes it.
331+ *
332+ * @param {string} url Address of the .js file to load
333+ * @param {function} callback Method to invoke when the script
334+ * has loaded and executed
335+ */
336+ function loadScript( url, callback ) {
337+
338+ var script = document.createElement( 'script' );
339+ script.type = 'text/javascript';
340+ script.async = false;
341+ script.defer = false;
342+ script.src = url;
343+
344+ if( callback ) {
345+
346+ // Success callback
347+ script.onload = script.onreadystatechange = function( event ) {
348+ if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
349+
350+ // Kill event listeners
351+ script.onload = script.onreadystatechange = script.onerror = null;
352+
353+ callback();
354+
355+ }
356+ };
357+
358+ // Error callback
359+ script.onerror = function( err ) {
360+
361+ // Kill event listeners
362+ script.onload = script.onreadystatechange = script.onerror = null;
363+
364+ callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
365+
366+ };
367+
368+ }
369+
370+ // Append the script at the end of <head>
371+ var head = document.querySelector( 'head' );
372+ head.insertBefore( script, head.lastChild );
373
374 }
375
376@@ -601,8 +751,7 @@
377 dom.speakerNotes.setAttribute( 'tabindex', '0' );
378
379 // Overlay graphic which is displayed during the paused mode
380- dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', '<button class="resume-button">Resume presentation</button>' );
381- dom.resumeButton = dom.pauseOverlay.querySelector( '.resume-button' );
382+ dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
383
384 dom.wrapper.setAttribute( 'role', 'application' );
385
386@@ -1082,18 +1231,27 @@
387 if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
388 if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
389
390- // If this slide has a background color, add a class that
391+ // If this slide has a background color, we add a class that
392 // signals if it is light or dark. If the slide has no background
393- // color, no class will be set
394- var computedBackgroundStyle = window.getComputedStyle( element );
395- if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
396- var rgb = colorToRgb( computedBackgroundStyle.backgroundColor );
397+ // color, no class will be added
398+ var contrastColor = data.backgroundColor;
399+
400+ // If no bg color was found, check the computed background
401+ if( !contrastColor ) {
402+ var computedBackgroundStyle = window.getComputedStyle( element );
403+ if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
404+ contrastColor = computedBackgroundStyle.backgroundColor;
405+ }
406+ }
407+
408+ if( contrastColor ) {
409+ var rgb = colorToRgb( contrastColor );
410
411 // Ignore fully transparent backgrounds. Some browsers return
412 // rgba(0,0,0,0) when reading the computed background color of
413 // an element with no background
414 if( rgb && rgb.a !== 0 ) {
415- if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {
416+ if( colorBrightness( contrastColor ) < 128 ) {
417 slide.classList.add( 'has-dark-background' );
418 }
419 else {
420@@ -1216,6 +1374,18 @@
421 disableRollingLinks();
422 }
423
424+ // Auto-hide the mouse pointer when its inactive
425+ if( config.hideInactiveCursor ) {
426+ document.addEventListener( 'mousemove', onDocumentCursorActive, false );
427+ document.addEventListener( 'mousedown', onDocumentCursorActive, false );
428+ }
429+ else {
430+ showCursor();
431+
432+ document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
433+ document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
434+ }
435+
436 // Iframe link previews
437 if( config.previewLinks ) {
438 enablePreviewLinks();
439@@ -1263,6 +1433,34 @@
440
441 dom.slideNumber.style.display = slideNumberDisplay;
442
443+ // Add the navigation mode to the DOM so we can adjust styling
444+ if( config.navigationMode !== 'default' ) {
445+ dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
446+ }
447+ else {
448+ dom.wrapper.removeAttribute( 'data-navigation-mode' );
449+ }
450+
451+ // Define our contextual list of keyboard shortcuts
452+ if( config.navigationMode === 'linear' ) {
453+ keyboardShortcuts['&#8594; , &#8595; , SPACE , N , L , J'] = 'Next slide';
454+ keyboardShortcuts['&#8592; , &#8593; , P , H , K'] = 'Previous slide';
455+ }
456+ else {
457+ keyboardShortcuts['N , SPACE'] = 'Next slide';
458+ keyboardShortcuts['P'] = 'Previous slide';
459+ keyboardShortcuts['&#8592; , H'] = 'Navigate left';
460+ keyboardShortcuts['&#8594; , L'] = 'Navigate right';
461+ keyboardShortcuts['&#8593; , K'] = 'Navigate up';
462+ keyboardShortcuts['&#8595; , J'] = 'Navigate down';
463+ }
464+
465+ keyboardShortcuts['Home , &#8984;/CTRL &#8592;'] = 'First slide';
466+ keyboardShortcuts['End , &#8984;/CTRL &#8594;'] = 'Last slide';
467+ keyboardShortcuts['B , .'] = 'Pause';
468+ keyboardShortcuts['F'] = 'Fullscreen';
469+ keyboardShortcuts['ESC, O'] = 'Slide overview';
470+
471 sync();
472
473 }
474@@ -1307,7 +1505,7 @@
475 dom.progress.addEventListener( 'click', onProgressClicked, false );
476 }
477
478- dom.resumeButton.addEventListener( 'click', resume, false );
479+ dom.pauseOverlay.addEventListener( 'click', resume, false );
480
481 if( config.focusBodyOnPageVisibilityChange ) {
482 var visibilityChange;
483@@ -1372,7 +1570,7 @@
484 dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
485 dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
486
487- dom.resumeButton.removeEventListener( 'click', resume, false );
488+ dom.pauseOverlay.removeEventListener( 'click', resume, false );
489
490 if ( config.progress && dom.progress ) {
491 dom.progress.removeEventListener( 'click', onProgressClicked, false );
492@@ -1390,6 +1588,53 @@
493 }
494
495 /**
496+ * Registers a new plugin with this reveal.js instance.
497+ *
498+ * reveal.js waits for all regisered plugins to initialize
499+ * before considering itself ready, as long as the plugin
500+ * is registered before calling `Reveal.initialize()`.
501+ */
502+ function registerPlugin( id, plugin ) {
503+
504+ if( plugins[id] === undefined ) {
505+ plugins[id] = plugin;
506+
507+ // If a plugin is registered after reveal.js is loaded,
508+ // initialize it right away
509+ if( loaded && typeof plugin.init === 'function' ) {
510+ plugin.init();
511+ }
512+ }
513+ else {
514+ console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
515+ }
516+
517+ }
518+
519+ /**
520+ * Checks if a specific plugin has been registered.
521+ *
522+ * @param {String} id Unique plugin identifier
523+ */
524+ function hasPlugin( id ) {
525+
526+ return !!plugins[id];
527+
528+ }
529+
530+ /**
531+ * Returns the specific plugin instance, if a plugin
532+ * with the given ID has been registered.
533+ *
534+ * @param {String} id Unique plugin identifier
535+ */
536+ function getPlugin( id ) {
537+
538+ return plugins[id];
539+
540+ }
541+
542+ /**
543 * Add a custom key binding with optional description to
544 * be added to the help screen.
545 */
546@@ -1677,11 +1922,19 @@
547 // Change the .stretch element height to 0 in order find the height of all
548 // the other elements
549 element.style.height = '0px';
550+
551+ // In Overview mode, the parent (.slide) height is set of 700px.
552+ // Restore it temporarily to its natural height.
553+ element.parentNode.style.height = 'auto';
554+
555 newHeight = height - element.parentNode.offsetHeight;
556
557 // Restore the old height, just in case
558 element.style.height = oldHeight + 'px';
559
560+ // Clear the parent (.slide) height. .removeProperty works in IE9+
561+ element.parentNode.style.removeProperty('height');
562+
563 return newHeight;
564 }
565
566@@ -1699,15 +1952,6 @@
567 }
568
569 /**
570- * Check if this instance is being used to print a PDF with fragments.
571- */
572- function isPrintingPDFFragments() {
573-
574- return ( /print-pdf-fragments/gi ).test( window.location.search );
575-
576- }
577-
578- /**
579 * Hides the address bar if we're on a mobile device.
580 */
581 function hideAddressBar() {
582@@ -1970,8 +2214,20 @@
583
584 if( !config.disableLayout ) {
585
586+ // On some mobile devices '100vh' is taller than the visible
587+ // viewport which leads to part of the presentation being
588+ // cut off. To work around this we define our own '--vh' custom
589+ // property where 100x adds up to the correct height.
590+ //
591+ // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
592+ if( isMobileDevice ) {
593+ document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
594+ }
595+
596 var size = getComputedSlideSize();
597
598+ var oldScale = scale;
599+
600 // Layout the contents of the slides
601 layoutSlideContents( config.width, config.height );
602
603@@ -2044,6 +2300,13 @@
604
605 }
606
607+ if( oldScale !== scale ) {
608+ dispatchEvent( 'resize', {
609+ 'oldScale': oldScale,
610+ 'scale': scale,
611+ 'size': size
612+ } );
613+ }
614 }
615
616 updateProgress();
617@@ -2443,6 +2706,32 @@
618 }
619
620 /**
621+ * Shows the mouse pointer after it has been hidden with
622+ * #hideCursor.
623+ */
624+ function showCursor() {
625+
626+ if( cursorHidden ) {
627+ cursorHidden = false;
628+ dom.wrapper.style.cursor = '';
629+ }
630+
631+ }
632+
633+ /**
634+ * Hides the mouse pointer when it's on top of the .reveal
635+ * container.
636+ */
637+ function hideCursor() {
638+
639+ if( cursorHidden === false ) {
640+ cursorHidden = true;
641+ dom.wrapper.style.cursor = 'none';
642+ }
643+
644+ }
645+
646+ /**
647 * Enters the paused mode which fades everything on screen to
648 * black.
649 */
650@@ -2584,28 +2873,6 @@
651
652 layout();
653
654- // Apply the new state
655- stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
656- // Check if this state existed on the previous slide. If it
657- // did, we will avoid adding it repeatedly
658- for( var j = 0; j < stateBefore.length; j++ ) {
659- if( stateBefore[j] === state[i] ) {
660- stateBefore.splice( j, 1 );
661- continue stateLoop;
662- }
663- }
664-
665- document.documentElement.classList.add( state[i] );
666-
667- // Dispatch custom event matching the state's name
668- dispatchEvent( state[i] );
669- }
670-
671- // Clean up the remains of the previous state
672- while( stateBefore.length ) {
673- document.documentElement.classList.remove( stateBefore.pop() );
674- }
675-
676 // Update the overview if it's currently active
677 if( isOverview() ) {
678 updateOverview();
679@@ -2654,6 +2921,28 @@
680 }
681 }
682
683+ // Apply the new state
684+ stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
685+ // Check if this state existed on the previous slide. If it
686+ // did, we will avoid adding it repeatedly
687+ for( var j = 0; j < stateBefore.length; j++ ) {
688+ if( stateBefore[j] === state[i] ) {
689+ stateBefore.splice( j, 1 );
690+ continue stateLoop;
691+ }
692+ }
693+
694+ document.documentElement.classList.add( state[i] );
695+
696+ // Dispatch custom event matching the state's name
697+ dispatchEvent( state[i] );
698+ }
699+
700+ // Clean up the remains of the previous state
701+ while( stateBefore.length ) {
702+ document.documentElement.classList.remove( stateBefore.pop() );
703+ }
704+
705 if( slideChanged ) {
706 dispatchEvent( 'slidechanged', {
707 'indexh': indexh,
708@@ -2679,6 +2968,7 @@
709 updateParallax();
710 updateSlideNumber();
711 updateNotes();
712+ updateFragments();
713
714 // Update the URL hash
715 writeURL();
716@@ -2751,6 +3041,9 @@
717 */
718 function syncSlide( slide ) {
719
720+ // Default to the current slide
721+ slide = slide || currentSlide;
722+
723 syncBackground( slide );
724 syncFragments( slide );
725
726@@ -2767,10 +3060,14 @@
727 * after reveal.js has already initialized.
728 *
729 * @param {HTMLElement} slide
730+ * @return {Array} a list of the HTML fragments that were synced
731 */
732 function syncFragments( slide ) {
733
734- sortFragments( slide.querySelectorAll( '.fragment' ) );
735+ // Default to the current slide
736+ slide = slide || currentSlide;
737+
738+ return sortFragments( slide.querySelectorAll( '.fragment' ) );
739
740 }
741
742@@ -2903,14 +3200,11 @@
743 element.classList.add( reverse ? 'future' : 'past' );
744
745 if( config.fragments ) {
746- var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
747-
748- // Show all fragments on prior slides
749- while( pastFragments.length ) {
750- var pastFragment = pastFragments.pop();
751- pastFragment.classList.add( 'visible' );
752- pastFragment.classList.remove( 'current-fragment' );
753- }
754+ // Show all fragments in prior slides
755+ toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
756+ fragment.classList.add( 'visible' );
757+ fragment.classList.remove( 'current-fragment' );
758+ } );
759 }
760 }
761 else if( i > index ) {
762@@ -2918,14 +3212,11 @@
763 element.classList.add( reverse ? 'past' : 'future' );
764
765 if( config.fragments ) {
766- var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
767-
768- // No fragments in future slides should be visible ahead of time
769- while( futureFragments.length ) {
770- var futureFragment = futureFragments.pop();
771- futureFragment.classList.remove( 'visible' );
772- futureFragment.classList.remove( 'current-fragment' );
773- }
774+ // Hide all fragments in future slides
775+ toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
776+ fragment.classList.remove( 'visible' );
777+ fragment.classList.remove( 'current-fragment' );
778+ } );
779 }
780 }
781 }
782@@ -3104,47 +3395,47 @@
783
784
785 /**
786- * Updates the slide number div to reflect the current slide.
787- *
788- * The following slide number formats are available:
789- * "h.v": horizontal . vertical slide number (default)
790- * "h/v": horizontal / vertical slide number
791- * "c": flattened slide number
792- * "c/t": flattened slide number / total slides
793+ * Updates the slide number to match the current slide.
794 */
795 function updateSlideNumber() {
796
797 // Update slide number if enabled
798 if( config.slideNumber && dom.slideNumber ) {
799
800- var value = [];
801+ var value;
802 var format = 'h.v';
803
804- // Check if a custom number format is available
805- if( typeof config.slideNumber === 'string' ) {
806- format = config.slideNumber;
807- }
808-
809- // If there are ONLY vertical slides in this deck, always use
810- // a flattened slide number
811- if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
812- format = 'c';
813- }
814-
815- switch( format ) {
816- case 'c':
817- value.push( getSlidePastCount() + 1 );
818- break;
819- case 'c/t':
820- value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
821- break;
822- case 'h/v':
823- value.push( indexh + 1 );
824- if( isVerticalSlide() ) value.push( '/', indexv + 1 );
825- break;
826- default:
827- value.push( indexh + 1 );
828- if( isVerticalSlide() ) value.push( '.', indexv + 1 );
829+ if( typeof config.slideNumber === 'function' ) {
830+ value = config.slideNumber();
831+ }
832+ else {
833+ // Check if a custom number format is available
834+ if( typeof config.slideNumber === 'string' ) {
835+ format = config.slideNumber;
836+ }
837+
838+ // If there are ONLY vertical slides in this deck, always use
839+ // a flattened slide number
840+ if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
841+ format = 'c';
842+ }
843+
844+ value = [];
845+ switch( format ) {
846+ case 'c':
847+ value.push( getSlidePastCount() + 1 );
848+ break;
849+ case 'c/t':
850+ value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
851+ break;
852+ case 'h/v':
853+ value.push( indexh + 1 );
854+ if( isVerticalSlide() ) value.push( '/', indexv + 1 );
855+ break;
856+ default:
857+ value.push( indexh + 1 );
858+ if( isVerticalSlide() ) value.push( '.', indexv + 1 );
859+ }
860 }
861
862 dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
863@@ -3428,6 +3719,26 @@
864 }
865
866 /**
867+ * Should the given element be preloaded?
868+ * Decides based on local element attributes and global config.
869+ *
870+ * @param {HTMLElement} element
871+ */
872+ function shouldPreload( element ) {
873+
874+ // Prefer an explicit global preload setting
875+ var preload = config.preloadIframes;
876+
877+ // If no global setting is available, fall back on the element's
878+ // own preload setting
879+ if( typeof preload !== 'boolean' ) {
880+ preload = element.hasAttribute( 'data-preload' );
881+ }
882+
883+ return preload;
884+ }
885+
886+ /**
887 * Called when the given slide is within the configured view
888 * distance. Shows the slide element and loads any content
889 * that is set to load lazily (data-src).
890@@ -3442,10 +3753,12 @@
891 slide.style.display = config.display;
892
893 // Media elements with data-src attributes
894- toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
895- element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
896- element.setAttribute( 'data-lazy-loaded', '' );
897- element.removeAttribute( 'data-src' );
898+ toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
899+ if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
900+ element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
901+ element.setAttribute( 'data-lazy-loaded', '' );
902+ element.removeAttribute( 'data-src' );
903+ }
904 } );
905
906 // Media elements with <source> children
907@@ -3563,7 +3876,7 @@
908 }
909
910 // Reset lazy-loaded media elements with src attributes
911- toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) {
912+ toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
913 element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
914 element.removeAttribute( 'src' );
915 } );
916@@ -3663,13 +3976,6 @@
917 _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
918 _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
919
920- // Always show media controls on mobile devices
921- if( isMobileDevice ) {
922- toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
923- el.controls = true;
924- } );
925- }
926-
927 }
928
929 /**
930@@ -3713,7 +4019,20 @@
931 // Mobile devices never fire a loaded event so instead
932 // of waiting, we initiate playback
933 else if( isMobileDevice ) {
934- el.play();
935+ var promise = el.play();
936+
937+ // If autoplay does not work, ensure that the controls are visible so
938+ // that the viewer can start the media on their own
939+ if( promise && typeof promise.catch === 'function' && el.controls === false ) {
940+ promise.catch( function() {
941+ el.controls = true;
942+
943+ // Once the video does start playing, hide the controls again
944+ el.addEventListener( 'play', function() {
945+ el.controls = false;
946+ } );
947+ } );
948+ }
949 }
950 // If the media isn't loaded, wait before playing
951 else {
952@@ -3947,7 +4266,7 @@
953
954 }
955
956- return pastCount / ( totalCount - 1 );
957+ return Math.min( pastCount / ( totalCount - 1 ), 1 );
958
959 }
960
961@@ -3974,9 +4293,9 @@
962 var bits = hash.slice( 2 ).split( '/' ),
963 name = hash.replace( /#|\//gi, '' );
964
965- // If the first bit is invalid and there is a name we can
966- // assume that this is a named link
967- if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
968+ // If the first bit is not fully numeric and there is a name we
969+ // can assume that this is a named link
970+ if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
971 var element;
972
973 // Ensure the named link is a valid HTML ID attribute
974@@ -3988,10 +4307,13 @@
975 // Ensure that we're not already on a slide with the same name
976 var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
977
978- if( element && !isSameNameAsCurrentSlide ) {
979- // Find the position of the named slide and navigate to it
980- var indices = Reveal.getIndices( element );
981- slide( indices.h, indices.v );
982+ if( element ) {
983+ // If the slide exists and is not the current slide...
984+ if ( !isSameNameAsCurrentSlide ) {
985+ // ...find the position of the named slide and navigate to it
986+ var indices = Reveal.getIndices(element);
987+ slide(indices.h, indices.v);
988+ }
989 }
990 // If the slide doesn't exist, navigate to the current slide
991 else {
992@@ -4029,18 +4351,30 @@
993 */
994 function writeURL( delay ) {
995
996- if( config.history ) {
997-
998- // Make sure there's never more than one timeout running
999- clearTimeout( writeURLTimeout );
1000-
1001- // If a delay is specified, timeout this call
1002- if( typeof delay === 'number' ) {
1003- writeURLTimeout = setTimeout( writeURL, delay );
1004- }
1005- else if( currentSlide ) {
1006+ // Make sure there's never more than one timeout running
1007+ clearTimeout( writeURLTimeout );
1008+
1009+ // If a delay is specified, timeout this call
1010+ if( typeof delay === 'number' ) {
1011+ writeURLTimeout = setTimeout( writeURL, delay );
1012+ }
1013+ else if( currentSlide ) {
1014+ // If we're configured to push to history OR the history
1015+ // API is not avaialble.
1016+ if( config.history || !window.history ) {
1017 window.location.hash = locationHash();
1018 }
1019+ // If we're configured to reflect the current slide in the
1020+ // URL without pushing to history.
1021+ else if( config.hash ) {
1022+ window.history.replaceState( null, null, '#' + locationHash() );
1023+ }
1024+ // If history and hash are both disabled, a hash may still
1025+ // be added to the URL by clicking on a href with a hash
1026+ // target. Counter this by always removing the hash.
1027+ else {
1028+ window.history.replaceState( null, null, window.location.pathname + window.location.search );
1029+ }
1030 }
1031
1032 }
1033@@ -4108,6 +4442,25 @@
1034 }
1035
1036 /**
1037+ * Returns an array of objects where each object represents the
1038+ * attributes on its respective slide.
1039+ */
1040+ function getSlidesAttributes() {
1041+
1042+ return getSlides().map( function( slide ) {
1043+
1044+ var attributes = {};
1045+ for( var i = 0; i < slide.attributes.length; i++ ) {
1046+ var attribute = slide.attributes[ i ];
1047+ attributes[ attribute.name ] = attribute.value;
1048+ }
1049+ return attributes;
1050+
1051+ } );
1052+
1053+ }
1054+
1055+ /**
1056 * Retrieves the total number of slides in this presentation.
1057 *
1058 * @return {number}
1059@@ -4300,6 +4653,73 @@
1060 }
1061
1062 /**
1063+ * Refreshes the fragments on the current slide so that they
1064+ * have the appropriate classes (.visible + .current-fragment).
1065+ *
1066+ * @param {number} [index] The index of the current fragment
1067+ * @param {array} [fragments] Array containing all fragments
1068+ * in the current slide
1069+ *
1070+ * @return {{shown: array, hidden: array}}
1071+ */
1072+ function updateFragments( index, fragments ) {
1073+
1074+ var changedFragments = {
1075+ shown: [],
1076+ hidden: []
1077+ };
1078+
1079+ if( currentSlide && config.fragments ) {
1080+
1081+ fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
1082+
1083+ if( fragments.length ) {
1084+
1085+ if( typeof index !== 'number' ) {
1086+ var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
1087+ if( currentFragment ) {
1088+ index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
1089+ }
1090+ }
1091+
1092+ toArray( fragments ).forEach( function( el, i ) {
1093+
1094+ if( el.hasAttribute( 'data-fragment-index' ) ) {
1095+ i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
1096+ }
1097+
1098+ // Visible fragments
1099+ if( i <= index ) {
1100+ if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
1101+ el.classList.add( 'visible' );
1102+ el.classList.remove( 'current-fragment' );
1103+
1104+ // Announce the fragments one by one to the Screen Reader
1105+ dom.statusDiv.textContent = getStatusText( el );
1106+
1107+ if( i === index ) {
1108+ el.classList.add( 'current-fragment' );
1109+ startEmbeddedContent( el );
1110+ }
1111+ }
1112+ // Hidden fragments
1113+ else {
1114+ if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
1115+ el.classList.remove( 'visible' );
1116+ el.classList.remove( 'current-fragment' );
1117+ }
1118+
1119+ } );
1120+
1121+ }
1122+
1123+ }
1124+
1125+ return changedFragments;
1126+
1127+ }
1128+
1129+ /**
1130 * Navigate to the specified slide fragment.
1131 *
1132 * @param {?number} index The index of the fragment that
1133@@ -4334,53 +4754,24 @@
1134 index += offset;
1135 }
1136
1137- var fragmentsShown = [],
1138- fragmentsHidden = [];
1139-
1140- toArray( fragments ).forEach( function( element, i ) {
1141-
1142- if( element.hasAttribute( 'data-fragment-index' ) ) {
1143- i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
1144- }
1145-
1146- // Visible fragments
1147- if( i <= index ) {
1148- if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
1149- element.classList.add( 'visible' );
1150- element.classList.remove( 'current-fragment' );
1151-
1152- // Announce the fragments one by one to the Screen Reader
1153- dom.statusDiv.textContent = getStatusText( element );
1154-
1155- if( i === index ) {
1156- element.classList.add( 'current-fragment' );
1157- startEmbeddedContent( element );
1158- }
1159- }
1160- // Hidden fragments
1161- else {
1162- if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
1163- element.classList.remove( 'visible' );
1164- element.classList.remove( 'current-fragment' );
1165- }
1166-
1167- } );
1168-
1169- if( fragmentsHidden.length ) {
1170- dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
1171+ var changedFragments = updateFragments( index, fragments );
1172+
1173+ if( changedFragments.hidden.length ) {
1174+ dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
1175 }
1176
1177- if( fragmentsShown.length ) {
1178- dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
1179+ if( changedFragments.shown.length ) {
1180+ dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
1181 }
1182
1183 updateControls();
1184 updateProgress();
1185+
1186 if( config.fragmentInURL ) {
1187 writeURL();
1188 }
1189
1190- return !!( fragmentsShown.length || fragmentsHidden.length );
1191+ return !!( changedFragments.shown.length || changedFragments.hidden.length );
1192
1193 }
1194
1195@@ -4527,12 +4918,12 @@
1196 // Reverse for RTL
1197 if( config.rtl ) {
1198 if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
1199- slide( indexh + 1 );
1200+ slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
1201 }
1202 }
1203 // Normal navigation
1204 else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
1205- slide( indexh - 1 );
1206+ slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
1207 }
1208
1209 }
1210@@ -4544,12 +4935,12 @@
1211 // Reverse for RTL
1212 if( config.rtl ) {
1213 if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
1214- slide( indexh - 1 );
1215+ slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
1216 }
1217 }
1218 // Normal navigation
1219 else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
1220- slide( indexh + 1 );
1221+ slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
1222 }
1223
1224 }
1225@@ -4676,6 +5067,22 @@
1226 }
1227
1228 /**
1229+ * Called whenever there is mouse input at the document level
1230+ * to determine if the cursor is active or not.
1231+ *
1232+ * @param {object} event
1233+ */
1234+ function onDocumentCursorActive( event ) {
1235+
1236+ showCursor();
1237+
1238+ clearTimeout( cursorInactiveTimeout );
1239+
1240+ cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
1241+
1242+ }
1243+
1244+ /**
1245 * Handler for the document level 'keypress' event.
1246 *
1247 * @param {object} event
1248@@ -4702,20 +5109,31 @@
1249 return true;
1250 }
1251
1252+ // Shorthand
1253+ var keyCode = event.keyCode;
1254+
1255 // Remember if auto-sliding was paused so we can toggle it
1256 var autoSlideWasPaused = autoSlidePaused;
1257
1258 onUserInput( event );
1259
1260- // Check if there's a focused element that could be using
1261- // the keyboard
1262+ // Is there a focused element that could be using the keyboard?
1263 var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
1264 var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
1265 var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
1266
1267+ // Whitelist specific modified + keycode combinations
1268+ var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
1269+ var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
1270+ var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
1271+
1272+ // Prevent all other events when a modifier is pressed
1273+ var unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
1274+ ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
1275+
1276 // Disregard the event if there's a focused element or a
1277 // keyboard modifier key is present
1278- if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
1279+ if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
1280
1281 // While paused only allow resume keyboard events; 'b', 'v', '.'
1282 var resumeKeyCodes = [66,86,190,191];
1283@@ -4730,7 +5148,7 @@
1284 }
1285 }
1286
1287- if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
1288+ if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
1289 return false;
1290 }
1291
1292@@ -4742,7 +5160,7 @@
1293 for( key in config.keyboard ) {
1294
1295 // Check if this binding matches the pressed key
1296- if( parseInt( key, 10 ) === event.keyCode ) {
1297+ if( parseInt( key, 10 ) === keyCode ) {
1298
1299 var value = config.keyboard[ key ];
1300
1301@@ -4769,7 +5187,7 @@
1302 for( key in registeredKeyBindings ) {
1303
1304 // Check if this binding matches the pressed key
1305- if( parseInt( key, 10 ) === event.keyCode ) {
1306+ if( parseInt( key, 10 ) === keyCode ) {
1307
1308 var action = registeredKeyBindings[ key ].callback;
1309
1310@@ -4793,35 +5211,92 @@
1311 // Assume true and try to prove false
1312 triggered = true;
1313
1314- switch( event.keyCode ) {
1315- // p, page up
1316- case 80: case 33: navigatePrev(); break;
1317- // n, page down
1318- case 78: case 34: navigateNext(); break;
1319- // h, left
1320- case 72: case 37: navigateLeft(); break;
1321- // l, right
1322- case 76: case 39: navigateRight(); break;
1323- // k, up
1324- case 75: case 38: navigateUp(); break;
1325- // j, down
1326- case 74: case 40: navigateDown(); break;
1327- // home
1328- case 36: slide( 0 ); break;
1329- // end
1330- case 35: slide( Number.MAX_VALUE ); break;
1331- // space
1332- case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
1333- // return
1334- case 13: isOverview() ? deactivateOverview() : triggered = false; break;
1335- // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button
1336- case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;
1337- // f
1338- case 70: enterFullscreen(); break;
1339- // a
1340- case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
1341- default:
1342- triggered = false;
1343+ // P, PAGE UP
1344+ if( keyCode === 80 || keyCode === 33 ) {
1345+ navigatePrev();
1346+ }
1347+ // N, PAGE DOWN
1348+ else if( keyCode === 78 || keyCode === 34 ) {
1349+ navigateNext();
1350+ }
1351+ // H, LEFT
1352+ else if( keyCode === 72 || keyCode === 37 ) {
1353+ if( firstSlideShortcut ) {
1354+ slide( 0 );
1355+ }
1356+ else if( !isOverview() && config.navigationMode === 'linear' ) {
1357+ navigatePrev();
1358+ }
1359+ else {
1360+ navigateLeft();
1361+ }
1362+ }
1363+ // L, RIGHT
1364+ else if( keyCode === 76 || keyCode === 39 ) {
1365+ if( lastSlideShortcut ) {
1366+ slide( Number.MAX_VALUE );
1367+ }
1368+ else if( !isOverview() && config.navigationMode === 'linear' ) {
1369+ navigateNext();
1370+ }
1371+ else {
1372+ navigateRight();
1373+ }
1374+ }
1375+ // K, UP
1376+ else if( keyCode === 75 || keyCode === 38 ) {
1377+ if( !isOverview() && config.navigationMode === 'linear' ) {
1378+ navigatePrev();
1379+ }
1380+ else {
1381+ navigateUp();
1382+ }
1383+ }
1384+ // J, DOWN
1385+ else if( keyCode === 74 || keyCode === 40 ) {
1386+ if( !isOverview() && config.navigationMode === 'linear' ) {
1387+ navigateNext();
1388+ }
1389+ else {
1390+ navigateDown();
1391+ }
1392+ }
1393+ // HOME
1394+ else if( keyCode === 36 ) {
1395+ slide( 0 );
1396+ }
1397+ // END
1398+ else if( keyCode === 35 ) {
1399+ slide( Number.MAX_VALUE );
1400+ }
1401+ // SPACE
1402+ else if( keyCode === 32 ) {
1403+ if( isOverview() ) {
1404+ deactivateOverview();
1405+ }
1406+ if( event.shiftKey ) {
1407+ navigatePrev();
1408+ }
1409+ else {
1410+ navigateNext();
1411+ }
1412+ }
1413+ // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
1414+ else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
1415+ togglePause();
1416+ }
1417+ // F
1418+ else if( keyCode === 70 ) {
1419+ enterFullscreen();
1420+ }
1421+ // A
1422+ else if( keyCode === 65 ) {
1423+ if ( config.autoSlideStoppable ) {
1424+ toggleAutoSlide( autoSlideWasPaused );
1425+ }
1426+ }
1427+ else {
1428+ triggered = false;
1429 }
1430
1431 }
1432@@ -4832,7 +5307,7 @@
1433 event.preventDefault && event.preventDefault();
1434 }
1435 // ESC or O key
1436- else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
1437+ else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
1438 if( dom.overlay ) {
1439 closeOverlay();
1440 }
1441@@ -4863,18 +5338,6 @@
1442 touch.startY = event.touches[0].clientY;
1443 touch.startCount = event.touches.length;
1444
1445- // If there's two touches we need to memorize the distance
1446- // between those two points to detect pinching
1447- if( event.touches.length === 2 && config.overview ) {
1448- touch.startSpan = distanceBetween( {
1449- x: event.touches[1].clientX,
1450- y: event.touches[1].clientY
1451- }, {
1452- x: touch.startX,
1453- y: touch.startY
1454- } );
1455- }
1456-
1457 }
1458
1459 /**
1460@@ -4893,37 +5356,8 @@
1461 var currentX = event.touches[0].clientX;
1462 var currentY = event.touches[0].clientY;
1463
1464- // If the touch started with two points and still has
1465- // two active touches; test for the pinch gesture
1466- if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
1467-
1468- // The current distance in pixels between the two touch points
1469- var currentSpan = distanceBetween( {
1470- x: event.touches[1].clientX,
1471- y: event.touches[1].clientY
1472- }, {
1473- x: touch.startX,
1474- y: touch.startY
1475- } );
1476-
1477- // If the span is larger than the desire amount we've got
1478- // ourselves a pinch
1479- if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
1480- touch.captured = true;
1481-
1482- if( currentSpan < touch.startSpan ) {
1483- activateOverview();
1484- }
1485- else {
1486- deactivateOverview();
1487- }
1488- }
1489-
1490- event.preventDefault();
1491-
1492- }
1493 // There was only one touch point, look for a swipe
1494- else if( event.touches.length === 1 && touch.startCount !== 2 ) {
1495+ if( event.touches.length === 1 && touch.startCount !== 2 ) {
1496
1497 var deltaX = currentX - touch.startX,
1498 deltaY = currentY - touch.startY;
1499@@ -5073,8 +5507,8 @@
1500 /**
1501 * Event handler for navigation control buttons.
1502 */
1503- function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
1504- function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
1505+ function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
1506+ function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
1507 function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
1508 function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
1509 function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
1510@@ -5464,6 +5898,10 @@
1511 // Returns an Array of all slides
1512 getSlides: getSlides,
1513
1514+ // Returns an Array of objects representing the attributes on
1515+ // the slides
1516+ getSlidesAttributes: getSlidesAttributes,
1517+
1518 // Returns the total number of slides
1519 getTotalSlides: getTotalSlides,
1520
1521@@ -5514,6 +5952,16 @@
1522 return query;
1523 },
1524
1525+ // Returns the top-level DOM element
1526+ getRevealElement: function() {
1527+ return dom.wrapper || document.querySelector( '.reveal' );
1528+ },
1529+
1530+ // Returns a hash with all registered plugins
1531+ getPlugins: function() {
1532+ return plugins;
1533+ },
1534+
1535 // Returns true if we're currently on the first slide
1536 isFirstSlide: function() {
1537 return ( indexh === 0 && indexv === 0 );
1538@@ -5555,22 +6003,25 @@
1539 // Forward event binding to the reveal DOM element
1540 addEventListener: function( type, listener, useCapture ) {
1541 if( 'addEventListener' in window ) {
1542- ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
1543+ Reveal.getRevealElement().addEventListener( type, listener, useCapture );
1544 }
1545 },
1546 removeEventListener: function( type, listener, useCapture ) {
1547 if( 'addEventListener' in window ) {
1548- ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
1549+ Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
1550 }
1551 },
1552
1553- // Adds a custom key binding
1554+ // Adds/removes a custom key binding
1555 addKeyBinding: addKeyBinding,
1556-
1557- // Removes a custom key binding
1558 removeKeyBinding: removeKeyBinding,
1559
1560- // Programatically triggers a keyboard event
1561+ // API for registering and retrieving plugins
1562+ registerPlugin: registerPlugin,
1563+ hasPlugin: hasPlugin,
1564+ getPlugin: getPlugin,
1565+
1566+ // Programmatically triggers a keyboard event
1567 triggerKey: function( keyCode ) {
1568 onDocumentKeyDown( { keyCode: keyCode } );
1569 },