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

Proposed by Tomas Groth
Status: Merged
Approved by: Raoul Snyman
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 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.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
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
=== modified file 'openlp/core/display/html/reveal.js'
--- openlp/core/display/html/reveal.js 2019-02-11 20:34:20 +0000
+++ openlp/core/display/html/reveal.js 2019-04-08 19:57:45 +0000
@@ -3,7 +3,7 @@
3 * http://revealjs.com3 * http://revealjs.com
4 * MIT licensed4 * MIT licensed
5 *5 *
6 * Copyright (C) 2018 Hakim El Hattab, http://hakim.se6 * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
7 */7 */
8(function( root, factory ) {8(function( root, factory ) {
9 if( typeof define === 'function' && define.amd ) {9 if( typeof define === 'function' && define.amd ) {
@@ -26,7 +26,7 @@
26 var Reveal;26 var Reveal;
2727
28 // The reveal.js version28 // The reveal.js version
29 var VERSION = '3.7.0';29 var VERSION = '3.8.0';
3030
31 var SLIDES_SELECTOR = '.slides section',31 var SLIDES_SELECTOR = '.slides section',
32 HORIZONTAL_SLIDES_SELECTOR = '.slides>section',32 HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
@@ -67,16 +67,36 @@
67 progress: true,67 progress: true,
6868
69 // Display the page number of the current slide69 // Display the page number of the current slide
70 // - true: Show slide number
71 // - false: Hide slide number
72 //
73 // Can optionally be set as a string that specifies the number formatting:
74 // - "h.v": Horizontal . vertical slide number (default)
75 // - "h/v": Horizontal / vertical slide number
76 // - "c": Flattened slide number
77 // - "c/t": Flattened slide number / total slides
78 //
79 // Alternatively, you can provide a function that returns the slide
80 // number for the current slide. The function needs to return an array
81 // with one string [slideNumber] or three strings [n1,delimiter,n2].
82 // See #formatSlideNumber().
70 slideNumber: false,83 slideNumber: false,
7184
85 // Can be used to limit the contexts in which the slide number appears
86 // - "all": Always show the slide number
87 // - "print": Only when printing to PDF
88 // - "speaker": Only in the speaker view
89 showSlideNumber: 'all',
90
72 // Use 1 based indexing for # links to match slide number (default is zero91 // Use 1 based indexing for # links to match slide number (default is zero
73 // based)92 // based)
74 hashOneBasedIndex: false,93 hashOneBasedIndex: false,
7594
76 // Determine which displays to show the slide number on95 // Add the current slide number to the URL hash so that reloading the
77 showSlideNumber: 'all',96 // page/copying the URL will return you to the same slide
97 hash: false,
7898
79 // Push each slide change to the browser history99 // Push each slide change to the browser history. Implies `hash: true`
80 history: false,100 history: false,
81101
82 // Enable keyboard shortcuts for navigation102 // Enable keyboard shortcuts for navigation
@@ -104,6 +124,32 @@
104 // Change the presentation direction to be RTL124 // Change the presentation direction to be RTL
105 rtl: false,125 rtl: false,
106126
127 // Changes the behavior of our navigation directions.
128 //
129 // "default"
130 // Left/right arrow keys step between horizontal slides, up/down
131 // arrow keys step between vertical slides. Space key steps through
132 // all slides (both horizontal and vertical).
133 //
134 // "linear"
135 // Removes the up/down arrows. Left/right arrows step through all
136 // slides (both horizontal and vertical).
137 //
138 // "grid"
139 // When this is enabled, stepping left/right from a vertical stack
140 // to an adjacent vertical stack will land you at the same vertical
141 // index.
142 //
143 // Consider a deck with six slides ordered in two vertical stacks:
144 // 1.1 2.1
145 // 1.2 2.2
146 // 1.3 2.3
147 //
148 // If you're on slide 1.3 and navigate right, you will normally move
149 // from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
150 // from 1.3 -> 2.3.
151 navigationMode: 'default',
152
107 // Randomizes the order of slides each time the presentation loads153 // Randomizes the order of slides each time the presentation loads
108 shuffle: false,154 shuffle: false,
109155
@@ -134,6 +180,13 @@
134 // - false: No media will autoplay, regardless of individual setting180 // - false: No media will autoplay, regardless of individual setting
135 autoPlayMedia: null,181 autoPlayMedia: null,
136182
183 // Global override for preloading lazy-loaded iframes
184 // - null: Iframes with data-src AND data-preload will be loaded when within
185 // the viewDistance, iframes with only data-src will be loaded when visible
186 // - true: All iframes with data-src will be loaded when within the viewDistance
187 // - false: All iframes with data-src will be loaded only when visible
188 preloadIframes: null,
189
137 // Controls automatic progression to the next slide190 // Controls automatic progression to the next slide
138 // - 0: Auto-sliding only happens if the data-autoslide HTML attribute191 // - 0: Auto-sliding only happens if the data-autoslide HTML attribute
139 // is present on the current slide or fragment192 // is present on the current slide or fragment
@@ -220,6 +273,12 @@
220 // The display mode that will be used to show slides273 // The display mode that will be used to show slides
221 display: 'block',274 display: 'block',
222275
276 // Hide cursor if inactive
277 hideInactiveCursor: true,
278
279 // Time before the cursor is hidden (in ms)
280 hideCursorTime: 5000,
281
223 // Script dependencies to load282 // Script dependencies to load
224 dependencies: []283 dependencies: []
225284
@@ -267,6 +326,12 @@
267 // Cached references to DOM elements326 // Cached references to DOM elements
268 dom = {},327 dom = {},
269328
329 // A list of registered reveal.js plugins
330 plugins = {},
331
332 // List of asynchronously loaded reveal.js dependencies
333 asyncDependencies = [],
334
270 // Features supported by the browser, see #checkCapabilities()335 // Features supported by the browser, see #checkCapabilities()
271 features = {},336 features = {},
272337
@@ -282,6 +347,12 @@
282 // Delays updates to the URL due to a Chrome thumbnailer bug347 // Delays updates to the URL due to a Chrome thumbnailer bug
283 writeURLTimeout = 0,348 writeURLTimeout = 0,
284349
350 // Is the mouse pointer currently hidden from view
351 cursorHidden = false,
352
353 // Timeout used to determine when the cursor is inactive
354 cursorInactiveTimeout = 0,
355
285 // Flags if the interaction event listeners are bound356 // Flags if the interaction event listeners are bound
286 eventsAreBound = false,357 eventsAreBound = false,
287358
@@ -298,26 +369,14 @@
298 touch = {369 touch = {
299 startX: 0,370 startX: 0,
300 startY: 0,371 startY: 0,
301 startSpan: 0,
302 startCount: 0,372 startCount: 0,
303 captured: false,373 captured: false,
304 threshold: 40374 threshold: 40
305 },375 },
306376
307 // Holds information about the keyboard shortcuts377 // A key:value map of shortcut keyboard keys and descriptions of
308 keyboardShortcuts = {378 // the actions they trigger, generated in #configure()
309 'N , SPACE': 'Next slide',379 keyboardShortcuts = {},
310 'P': 'Previous slide',
311 '← , H': 'Navigate left',
312 '→ , L': 'Navigate right',
313 '↑ , K': 'Navigate up',
314 '↓ , J': 'Navigate down',
315 'Home': 'First slide',
316 'End': 'Last slide',
317 'B , .': 'Pause',
318 'F': 'Fullscreen',
319 'ESC, O': 'Slide overview'
320 },
321380
322 // Holds custom key code mappings381 // Holds custom key code mappings
323 registeredKeyBindings = {};382 registeredKeyBindings = {};
@@ -377,7 +436,7 @@
377 // Hide the address bar in mobile browsers436 // Hide the address bar in mobile browsers
378 hideAddressBar();437 hideAddressBar();
379438
380 // Loads the dependencies and continues to #start() once done439 // Loads dependencies and continues to #start() once done
381 load();440 load();
382441
383 }442 }
@@ -440,57 +499,148 @@
440 function load() {499 function load() {
441500
442 var scripts = [],501 var scripts = [],
443 scriptsAsync = [],502 scriptsToLoad = 0;
444 scriptsToPreload = 0;503
445504 config.dependencies.forEach( function( s ) {
446 // Called once synchronous scripts finish loading
447 function proceed() {
448 if( scriptsAsync.length ) {
449 // Load asynchronous scripts
450 head.js.apply( null, scriptsAsync );
451 }
452
453 start();
454 }
455
456 function loadScript( s ) {
457 head.ready( s.src.match( /([\w\d_\-]*)\.?js(\?[\w\d.=&]*)?$|[^\\\/]*$/i )[0], function() {
458 // Extension may contain callback functions
459 if( typeof s.callback === 'function' ) {
460 s.callback.apply( this );
461 }
462
463 if( --scriptsToPreload === 0 ) {
464 proceed();
465 }
466 });
467 }
468
469 for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
470 var s = config.dependencies[i];
471
472 // Load if there's no condition or the condition is truthy505 // Load if there's no condition or the condition is truthy
473 if( !s.condition || s.condition() ) {506 if( !s.condition || s.condition() ) {
474 if( s.async ) {507 if( s.async ) {
475 scriptsAsync.push( s.src );508 asyncDependencies.push( s );
476 }509 }
477 else {510 else {
478 scripts.push( s.src );511 scripts.push( s );
479 }512 }
480
481 loadScript( s );
482 }513 }
483 }514 } );
484515
485 if( scripts.length ) {516 if( scripts.length ) {
486 scriptsToPreload = scripts.length;517 scriptsToLoad = scripts.length;
487518
488 // Load synchronous scripts519 // Load synchronous scripts
489 head.js.apply( null, scripts );520 scripts.forEach( function( s ) {
490 }521 loadScript( s.src, function() {
491 else {522
492 proceed();523 if( typeof s.callback === 'function' ) s.callback();
493 }524
525 if( --scriptsToLoad === 0 ) {
526 initPlugins();
527 }
528
529 } );
530 } );
531 }
532 else {
533 initPlugins();
534 }
535
536 }
537
538 /**
539 * Initializes our plugins and waits for them to be ready
540 * before proceeding.
541 */
542 function initPlugins() {
543
544 var pluginsToInitialize = Object.keys( plugins ).length;
545
546 // If there are no plugins, skip this step
547 if( pluginsToInitialize === 0 ) {
548 loadAsyncDependencies();
549 }
550 // ... otherwise initialize plugins
551 else {
552
553 var afterPlugInitialized = function() {
554 if( --pluginsToInitialize === 0 ) {
555 loadAsyncDependencies();
556 }
557 };
558
559 for( var i in plugins ) {
560
561 var plugin = plugins[i];
562
563 // If the plugin has an 'init' method, invoke it
564 if( typeof plugin.init === 'function' ) {
565 var callback = plugin.init();
566
567 // If the plugin returned a Promise, wait for it
568 if( callback && typeof callback.then === 'function' ) {
569 callback.then( afterPlugInitialized );
570 }
571 else {
572 afterPlugInitialized();
573 }
574 }
575 else {
576 afterPlugInitialized();
577 }
578
579 }
580
581 }
582
583 }
584
585 /**
586 * Loads all async reveal.js dependencies.
587 */
588 function loadAsyncDependencies() {
589
590 if( asyncDependencies.length ) {
591 asyncDependencies.forEach( function( s ) {
592 loadScript( s.src, s.callback );
593 } );
594 }
595
596 start();
597
598 }
599
600 /**
601 * Loads a JavaScript file from the given URL and executes it.
602 *
603 * @param {string} url Address of the .js file to load
604 * @param {function} callback Method to invoke when the script
605 * has loaded and executed
606 */
607 function loadScript( url, callback ) {
608
609 var script = document.createElement( 'script' );
610 script.type = 'text/javascript';
611 script.async = false;
612 script.defer = false;
613 script.src = url;
614
615 if( callback ) {
616
617 // Success callback
618 script.onload = script.onreadystatechange = function( event ) {
619 if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
620
621 // Kill event listeners
622 script.onload = script.onreadystatechange = script.onerror = null;
623
624 callback();
625
626 }
627 };
628
629 // Error callback
630 script.onerror = function( err ) {
631
632 // Kill event listeners
633 script.onload = script.onreadystatechange = script.onerror = null;
634
635 callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
636
637 };
638
639 }
640
641 // Append the script at the end of <head>
642 var head = document.querySelector( 'head' );
643 head.insertBefore( script, head.lastChild );
494644
495 }645 }
496646
@@ -601,8 +751,7 @@
601 dom.speakerNotes.setAttribute( 'tabindex', '0' );751 dom.speakerNotes.setAttribute( 'tabindex', '0' );
602752
603 // Overlay graphic which is displayed during the paused mode753 // Overlay graphic which is displayed during the paused mode
604 dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', '<button class="resume-button">Resume presentation</button>' );754 dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
605 dom.resumeButton = dom.pauseOverlay.querySelector( '.resume-button' );
606755
607 dom.wrapper.setAttribute( 'role', 'application' );756 dom.wrapper.setAttribute( 'role', 'application' );
608757
@@ -1082,18 +1231,27 @@
1082 if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;1231 if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
1083 if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;1232 if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
10841233
1085 // If this slide has a background color, add a class that1234 // If this slide has a background color, we add a class that
1086 // signals if it is light or dark. If the slide has no background1235 // signals if it is light or dark. If the slide has no background
1087 // color, no class will be set1236 // color, no class will be added
1088 var computedBackgroundStyle = window.getComputedStyle( element );1237 var contrastColor = data.backgroundColor;
1089 if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {1238
1090 var rgb = colorToRgb( computedBackgroundStyle.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;
1244 }
1245 }
1246
1247 if( contrastColor ) {
1248 var rgb = colorToRgb( contrastColor );
10911249
1092 // Ignore fully transparent backgrounds. Some browsers return1250 // Ignore fully transparent backgrounds. Some browsers return
1093 // rgba(0,0,0,0) when reading the computed background color of1251 // rgba(0,0,0,0) when reading the computed background color of
1094 // an element with no background1252 // an element with no background
1095 if( rgb && rgb.a !== 0 ) {1253 if( rgb && rgb.a !== 0 ) {
1096 if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {1254 if( colorBrightness( contrastColor ) < 128 ) {
1097 slide.classList.add( 'has-dark-background' );1255 slide.classList.add( 'has-dark-background' );
1098 }1256 }
1099 else {1257 else {
@@ -1216,6 +1374,18 @@
1216 disableRollingLinks();1374 disableRollingLinks();
1217 }1375 }
12181376
1377 // Auto-hide the mouse pointer when its inactive
1378 if( config.hideInactiveCursor ) {
1379 document.addEventListener( 'mousemove', onDocumentCursorActive, false );
1380 document.addEventListener( 'mousedown', onDocumentCursorActive, false );
1381 }
1382 else {
1383 showCursor();
1384
1385 document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
1386 document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
1387 }
1388
1219 // Iframe link previews1389 // Iframe link previews
1220 if( config.previewLinks ) {1390 if( config.previewLinks ) {
1221 enablePreviewLinks();1391 enablePreviewLinks();
@@ -1263,6 +1433,34 @@
12631433
1264 dom.slideNumber.style.display = slideNumberDisplay;1434 dom.slideNumber.style.display = slideNumberDisplay;
12651435
1436 // Add the navigation mode to the DOM so we can adjust styling
1437 if( config.navigationMode !== 'default' ) {
1438 dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
1439 }
1440 else {
1441 dom.wrapper.removeAttribute( 'data-navigation-mode' );
1442 }
1443
1444 // Define our contextual list of keyboard shortcuts
1445 if( config.navigationMode === 'linear' ) {
1446 keyboardShortcuts['&#8594; , &#8595; , SPACE , N , L , J'] = 'Next slide';
1447 keyboardShortcuts['&#8592; , &#8593; , P , H , K'] = 'Previous slide';
1448 }
1449 else {
1450 keyboardShortcuts['N , SPACE'] = 'Next slide';
1451 keyboardShortcuts['P'] = 'Previous slide';
1452 keyboardShortcuts['&#8592; , H'] = 'Navigate left';
1453 keyboardShortcuts['&#8594; , L'] = 'Navigate right';
1454 keyboardShortcuts['&#8593; , K'] = 'Navigate up';
1455 keyboardShortcuts['&#8595; , J'] = 'Navigate down';
1456 }
1457
1458 keyboardShortcuts['Home , &#8984;/CTRL &#8592;'] = 'First slide';
1459 keyboardShortcuts['End , &#8984;/CTRL &#8594;'] = 'Last slide';
1460 keyboardShortcuts['B , .'] = 'Pause';
1461 keyboardShortcuts['F'] = 'Fullscreen';
1462 keyboardShortcuts['ESC, O'] = 'Slide overview';
1463
1266 sync();1464 sync();
12671465
1268 }1466 }
@@ -1307,7 +1505,7 @@
1307 dom.progress.addEventListener( 'click', onProgressClicked, false );1505 dom.progress.addEventListener( 'click', onProgressClicked, false );
1308 }1506 }
13091507
1310 dom.resumeButton.addEventListener( 'click', resume, false );1508 dom.pauseOverlay.addEventListener( 'click', resume, false );
13111509
1312 if( config.focusBodyOnPageVisibilityChange ) {1510 if( config.focusBodyOnPageVisibilityChange ) {
1313 var visibilityChange;1511 var visibilityChange;
@@ -1372,7 +1570,7 @@
1372 dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );1570 dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
1373 dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );1571 dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
13741572
1375 dom.resumeButton.removeEventListener( 'click', resume, false );1573 dom.pauseOverlay.removeEventListener( 'click', resume, false );
13761574
1377 if ( config.progress && dom.progress ) {1575 if ( config.progress && dom.progress ) {
1378 dom.progress.removeEventListener( 'click', onProgressClicked, false );1576 dom.progress.removeEventListener( 'click', onProgressClicked, false );
@@ -1390,6 +1588,53 @@
1390 }1588 }
13911589
1392 /**1590 /**
1591 * Registers a new plugin with this reveal.js instance.
1592 *
1593 * reveal.js waits for all regisered plugins to initialize
1594 * before considering itself ready, as long as the plugin
1595 * is registered before calling `Reveal.initialize()`.
1596 */
1597 function registerPlugin( id, plugin ) {
1598
1599 if( plugins[id] === undefined ) {
1600 plugins[id] = plugin;
1601
1602 // If a plugin is registered after reveal.js is loaded,
1603 // initialize it right away
1604 if( loaded && typeof plugin.init === 'function' ) {
1605 plugin.init();
1606 }
1607 }
1608 else {
1609 console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
1610 }
1611
1612 }
1613
1614 /**
1615 * Checks if a specific plugin has been registered.
1616 *
1617 * @param {String} id Unique plugin identifier
1618 */
1619 function hasPlugin( id ) {
1620
1621 return !!plugins[id];
1622
1623 }
1624
1625 /**
1626 * Returns the specific plugin instance, if a plugin
1627 * with the given ID has been registered.
1628 *
1629 * @param {String} id Unique plugin identifier
1630 */
1631 function getPlugin( id ) {
1632
1633 return plugins[id];
1634
1635 }
1636
1637 /**
1393 * Add a custom key binding with optional description to1638 * Add a custom key binding with optional description to
1394 * be added to the help screen.1639 * be added to the help screen.
1395 */1640 */
@@ -1677,11 +1922,19 @@
1677 // Change the .stretch element height to 0 in order find the height of all1922 // Change the .stretch element height to 0 in order find the height of all
1678 // the other elements1923 // the other elements
1679 element.style.height = '0px';1924 element.style.height = '0px';
1925
1926 // In Overview mode, the parent (.slide) height is set of 700px.
1927 // Restore it temporarily to its natural height.
1928 element.parentNode.style.height = 'auto';
1929
1680 newHeight = height - element.parentNode.offsetHeight;1930 newHeight = height - element.parentNode.offsetHeight;
16811931
1682 // Restore the old height, just in case1932 // Restore the old height, just in case
1683 element.style.height = oldHeight + 'px';1933 element.style.height = oldHeight + 'px';
16841934
1935 // Clear the parent (.slide) height. .removeProperty works in IE9+
1936 element.parentNode.style.removeProperty('height');
1937
1685 return newHeight;1938 return newHeight;
1686 }1939 }
16871940
@@ -1699,15 +1952,6 @@
1699 }1952 }
17001953
1701 /**1954 /**
1702 * Check if this instance is being used to print a PDF with fragments.
1703 */
1704 function isPrintingPDFFragments() {
1705
1706 return ( /print-pdf-fragments/gi ).test( window.location.search );
1707
1708 }
1709
1710 /**
1711 * Hides the address bar if we're on a mobile device.1955 * Hides the address bar if we're on a mobile device.
1712 */1956 */
1713 function hideAddressBar() {1957 function hideAddressBar() {
@@ -1970,8 +2214,20 @@
19702214
1971 if( !config.disableLayout ) {2215 if( !config.disableLayout ) {
19722216
2217 // On some mobile devices '100vh' is taller than the visible
2218 // viewport which leads to part of the presentation being
2219 // cut off. To work around this we define our own '--vh' custom
2220 // property where 100x adds up to the correct height.
2221 //
2222 // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
2223 if( isMobileDevice ) {
2224 document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
2225 }
2226
1973 var size = getComputedSlideSize();2227 var size = getComputedSlideSize();
19742228
2229 var oldScale = scale;
2230
1975 // Layout the contents of the slides2231 // Layout the contents of the slides
1976 layoutSlideContents( config.width, config.height );2232 layoutSlideContents( config.width, config.height );
19772233
@@ -2044,6 +2300,13 @@
20442300
2045 }2301 }
20462302
2303 if( oldScale !== scale ) {
2304 dispatchEvent( 'resize', {
2305 'oldScale': oldScale,
2306 'scale': scale,
2307 'size': size
2308 } );
2309 }
2047 }2310 }
20482311
2049 updateProgress();2312 updateProgress();
@@ -2443,6 +2706,32 @@
2443 }2706 }
24442707
2445 /**2708 /**
2709 * Shows the mouse pointer after it has been hidden with
2710 * #hideCursor.
2711 */
2712 function showCursor() {
2713
2714 if( cursorHidden ) {
2715 cursorHidden = false;
2716 dom.wrapper.style.cursor = '';
2717 }
2718
2719 }
2720
2721 /**
2722 * Hides the mouse pointer when it's on top of the .reveal
2723 * container.
2724 */
2725 function hideCursor() {
2726
2727 if( cursorHidden === false ) {
2728 cursorHidden = true;
2729 dom.wrapper.style.cursor = 'none';
2730 }
2731
2732 }
2733
2734 /**
2446 * Enters the paused mode which fades everything on screen to2735 * Enters the paused mode which fades everything on screen to
2447 * black.2736 * black.
2448 */2737 */
@@ -2584,28 +2873,6 @@
25842873
2585 layout();2874 layout();
25862875
2587 // Apply the new state
2588 stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
2589 // Check if this state existed on the previous slide. If it
2590 // did, we will avoid adding it repeatedly
2591 for( var j = 0; j < stateBefore.length; j++ ) {
2592 if( stateBefore[j] === state[i] ) {
2593 stateBefore.splice( j, 1 );
2594 continue stateLoop;
2595 }
2596 }
2597
2598 document.documentElement.classList.add( state[i] );
2599
2600 // Dispatch custom event matching the state's name
2601 dispatchEvent( state[i] );
2602 }
2603
2604 // Clean up the remains of the previous state
2605 while( stateBefore.length ) {
2606 document.documentElement.classList.remove( stateBefore.pop() );
2607 }
2608
2609 // Update the overview if it's currently active2876 // Update the overview if it's currently active
2610 if( isOverview() ) {2877 if( isOverview() ) {
2611 updateOverview();2878 updateOverview();
@@ -2654,6 +2921,28 @@
2654 }2921 }
2655 }2922 }
26562923
2924 // Apply the new state
2925 stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
2926 // Check if this state existed on the previous slide. If it
2927 // did, we will avoid adding it repeatedly
2928 for( var j = 0; j < stateBefore.length; j++ ) {
2929 if( stateBefore[j] === state[i] ) {
2930 stateBefore.splice( j, 1 );
2931 continue stateLoop;
2932 }
2933 }
2934
2935 document.documentElement.classList.add( state[i] );
2936
2937 // Dispatch custom event matching the state's name
2938 dispatchEvent( state[i] );
2939 }
2940
2941 // Clean up the remains of the previous state
2942 while( stateBefore.length ) {
2943 document.documentElement.classList.remove( stateBefore.pop() );
2944 }
2945
2657 if( slideChanged ) {2946 if( slideChanged ) {
2658 dispatchEvent( 'slidechanged', {2947 dispatchEvent( 'slidechanged', {
2659 'indexh': indexh,2948 'indexh': indexh,
@@ -2679,6 +2968,7 @@
2679 updateParallax();2968 updateParallax();
2680 updateSlideNumber();2969 updateSlideNumber();
2681 updateNotes();2970 updateNotes();
2971 updateFragments();
26822972
2683 // Update the URL hash2973 // Update the URL hash
2684 writeURL();2974 writeURL();
@@ -2751,6 +3041,9 @@
2751 */3041 */
2752 function syncSlide( slide ) {3042 function syncSlide( slide ) {
27533043
3044 // Default to the current slide
3045 slide = slide || currentSlide;
3046
2754 syncBackground( slide );3047 syncBackground( slide );
2755 syncFragments( slide );3048 syncFragments( slide );
27563049
@@ -2767,10 +3060,14 @@
2767 * after reveal.js has already initialized.3060 * after reveal.js has already initialized.
2768 *3061 *
2769 * @param {HTMLElement} slide3062 * @param {HTMLElement} slide
3063 * @return {Array} a list of the HTML fragments that were synced
2770 */3064 */
2771 function syncFragments( slide ) {3065 function syncFragments( slide ) {
27723066
2773 sortFragments( slide.querySelectorAll( '.fragment' ) );3067 // Default to the current slide
3068 slide = slide || currentSlide;
3069
3070 return sortFragments( slide.querySelectorAll( '.fragment' ) );
27743071
2775 }3072 }
27763073
@@ -2903,14 +3200,11 @@
2903 element.classList.add( reverse ? 'future' : 'past' );3200 element.classList.add( reverse ? 'future' : 'past' );
29043201
2905 if( config.fragments ) {3202 if( config.fragments ) {
2906 var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );3203 // Show all fragments in prior slides
29073204 toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
2908 // Show all fragments on prior slides3205 fragment.classList.add( 'visible' );
2909 while( pastFragments.length ) {3206 fragment.classList.remove( 'current-fragment' );
2910 var pastFragment = pastFragments.pop();3207 } );
2911 pastFragment.classList.add( 'visible' );
2912 pastFragment.classList.remove( 'current-fragment' );
2913 }
2914 }3208 }
2915 }3209 }
2916 else if( i > index ) {3210 else if( i > index ) {
@@ -2918,14 +3212,11 @@
2918 element.classList.add( reverse ? 'past' : 'future' );3212 element.classList.add( reverse ? 'past' : 'future' );
29193213
2920 if( config.fragments ) {3214 if( config.fragments ) {
2921 var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );3215 // Hide all fragments in future slides
29223216 toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
2923 // No fragments in future slides should be visible ahead of time3217 fragment.classList.remove( 'visible' );
2924 while( futureFragments.length ) {3218 fragment.classList.remove( 'current-fragment' );
2925 var futureFragment = futureFragments.pop();3219 } );
2926 futureFragment.classList.remove( 'visible' );
2927 futureFragment.classList.remove( 'current-fragment' );
2928 }
2929 }3220 }
2930 }3221 }
2931 }3222 }
@@ -3104,47 +3395,47 @@
31043395
31053396
3106 /**3397 /**
3107 * Updates the slide number div to reflect the current slide.3398 * Updates the slide number to match the current slide.
3108 *
3109 * The following slide number formats are available:
3110 * "h.v": horizontal . vertical slide number (default)
3111 * "h/v": horizontal / vertical slide number
3112 * "c": flattened slide number
3113 * "c/t": flattened slide number / total slides
3114 */3399 */
3115 function updateSlideNumber() {3400 function updateSlideNumber() {
31163401
3117 // Update slide number if enabled3402 // Update slide number if enabled
3118 if( config.slideNumber && dom.slideNumber ) {3403 if( config.slideNumber && dom.slideNumber ) {
31193404
3120 var value = [];3405 var value;
3121 var format = 'h.v';3406 var format = 'h.v';
31223407
3123 // Check if a custom number format is available3408 if( typeof config.slideNumber === 'function' ) {
3124 if( typeof config.slideNumber === 'string' ) {3409 value = config.slideNumber();
3125 format = config.slideNumber;3410 }
3126 }3411 else {
31273412 // Check if a custom number format is available
3128 // If there are ONLY vertical slides in this deck, always use3413 if( typeof config.slideNumber === 'string' ) {
3129 // a flattened slide number3414 format = config.slideNumber;
3130 if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {3415 }
3131 format = 'c';3416
3132 }3417 // If there are ONLY vertical slides in this deck, always use
31333418 // a flattened slide number
3134 switch( format ) {3419 if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
3135 case 'c':3420 format = 'c';
3136 value.push( getSlidePastCount() + 1 );3421 }
3137 break;3422
3138 case 'c/t':3423 value = [];
3139 value.push( getSlidePastCount() + 1, '/', getTotalSlides() );3424 switch( format ) {
3140 break;3425 case 'c':
3141 case 'h/v':3426 value.push( getSlidePastCount() + 1 );
3142 value.push( indexh + 1 );3427 break;
3143 if( isVerticalSlide() ) value.push( '/', indexv + 1 );3428 case 'c/t':
3144 break;3429 value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
3145 default:3430 break;
3146 value.push( indexh + 1 );3431 case 'h/v':
3147 if( isVerticalSlide() ) value.push( '.', indexv + 1 );3432 value.push( indexh + 1 );
3433 if( isVerticalSlide() ) value.push( '/', indexv + 1 );
3434 break;
3435 default:
3436 value.push( indexh + 1 );
3437 if( isVerticalSlide() ) value.push( '.', indexv + 1 );
3438 }
3148 }3439 }
31493440
3150 dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );3441 dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
@@ -3428,6 +3719,26 @@
3428 }3719 }
34293720
3430 /**3721 /**
3722 * Should the given element be preloaded?
3723 * Decides based on local element attributes and global config.
3724 *
3725 * @param {HTMLElement} element
3726 */
3727 function shouldPreload( element ) {
3728
3729 // Prefer an explicit global preload setting
3730 var preload = config.preloadIframes;
3731
3732 // If no global setting is available, fall back on the element's
3733 // own preload setting
3734 if( typeof preload !== 'boolean' ) {
3735 preload = element.hasAttribute( 'data-preload' );
3736 }
3737
3738 return preload;
3739 }
3740
3741 /**
3431 * Called when the given slide is within the configured view3742 * Called when the given slide is within the configured view
3432 * distance. Shows the slide element and loads any content3743 * distance. Shows the slide element and loads any content
3433 * that is set to load lazily (data-src).3744 * that is set to load lazily (data-src).
@@ -3442,10 +3753,12 @@
3442 slide.style.display = config.display;3753 slide.style.display = config.display;
34433754
3444 // Media elements with data-src attributes3755 // Media elements with data-src attributes
3445 toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {3756 toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
3446 element.setAttribute( 'src', element.getAttribute( 'data-src' ) );3757 if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
3447 element.setAttribute( 'data-lazy-loaded', '' );3758 element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
3448 element.removeAttribute( 'data-src' );3759 element.setAttribute( 'data-lazy-loaded', '' );
3760 element.removeAttribute( 'data-src' );
3761 }
3449 } );3762 } );
34503763
3451 // Media elements with <source> children3764 // Media elements with <source> children
@@ -3563,7 +3876,7 @@
3563 }3876 }
35643877
3565 // Reset lazy-loaded media elements with src attributes3878 // Reset lazy-loaded media elements with src attributes
3566 toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) {3879 toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
3567 element.setAttribute( 'data-src', element.getAttribute( 'src' ) );3880 element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
3568 element.removeAttribute( 'src' );3881 element.removeAttribute( 'src' );
3569 } );3882 } );
@@ -3663,13 +3976,6 @@
3663 _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );3976 _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
3664 _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );3977 _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
36653978
3666 // Always show media controls on mobile devices
3667 if( isMobileDevice ) {
3668 toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3669 el.controls = true;
3670 } );
3671 }
3672
3673 }3979 }
36743980
3675 /**3981 /**
@@ -3713,7 +4019,20 @@
3713 // Mobile devices never fire a loaded event so instead4019 // Mobile devices never fire a loaded event so instead
3714 // of waiting, we initiate playback4020 // of waiting, we initiate playback
3715 else if( isMobileDevice ) {4021 else if( isMobileDevice ) {
3716 el.play();4022 var promise = el.play();
4023
4024 // If autoplay does not work, ensure that the controls are visible so
4025 // that the viewer can start the media on their own
4026 if( promise && typeof promise.catch === 'function' && el.controls === false ) {
4027 promise.catch( function() {
4028 el.controls = true;
4029
4030 // Once the video does start playing, hide the controls again
4031 el.addEventListener( 'play', function() {
4032 el.controls = false;
4033 } );
4034 } );
4035 }
3717 }4036 }
3718 // If the media isn't loaded, wait before playing4037 // If the media isn't loaded, wait before playing
3719 else {4038 else {
@@ -3947,7 +4266,7 @@
39474266
3948 }4267 }
39494268
3950 return pastCount / ( totalCount - 1 );4269 return Math.min( pastCount / ( totalCount - 1 ), 1 );
39514270
3952 }4271 }
39534272
@@ -3974,9 +4293,9 @@
3974 var bits = hash.slice( 2 ).split( '/' ),4293 var bits = hash.slice( 2 ).split( '/' ),
3975 name = hash.replace( /#|\//gi, '' );4294 name = hash.replace( /#|\//gi, '' );
39764295
3977 // If the first bit is invalid and there is a name we can4296 // If the first bit is not fully numeric and there is a name we
3978 // assume that this is a named link4297 // can assume that this is a named link
3979 if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {4298 if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
3980 var element;4299 var element;
39814300
3982 // Ensure the named link is a valid HTML ID attribute4301 // Ensure the named link is a valid HTML ID attribute
@@ -3988,10 +4307,13 @@
3988 // Ensure that we're not already on a slide with the same name4307 // Ensure that we're not already on a slide with the same name
3989 var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;4308 var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
39904309
3991 if( element && !isSameNameAsCurrentSlide ) {4310 if( element ) {
3992 // Find the position of the named slide and navigate to it4311 // If the slide exists and is not the current slide...
3993 var indices = Reveal.getIndices( element );4312 if ( !isSameNameAsCurrentSlide ) {
3994 slide( indices.h, indices.v );4313 // ...find the position of the named slide and navigate to it
4314 var indices = Reveal.getIndices(element);
4315 slide(indices.h, indices.v);
4316 }
3995 }4317 }
3996 // If the slide doesn't exist, navigate to the current slide4318 // If the slide doesn't exist, navigate to the current slide
3997 else {4319 else {
@@ -4029,18 +4351,30 @@
4029 */4351 */
4030 function writeURL( delay ) {4352 function writeURL( delay ) {
40314353
4032 if( config.history ) {4354 // Make sure there's never more than one timeout running
40334355 clearTimeout( writeURLTimeout );
4034 // Make sure there's never more than one timeout running4356
4035 clearTimeout( writeURLTimeout );4357 // If a delay is specified, timeout this call
40364358 if( typeof delay === 'number' ) {
4037 // If a delay is specified, timeout this call4359 writeURLTimeout = setTimeout( writeURL, delay );
4038 if( typeof delay === 'number' ) {4360 }
4039 writeURLTimeout = setTimeout( writeURL, delay );4361 else if( currentSlide ) {
4040 }4362 // If we're configured to push to history OR the history
4041 else if( currentSlide ) {4363 // API is not avaialble.
4364 if( config.history || !window.history ) {
4042 window.location.hash = locationHash();4365 window.location.hash = locationHash();
4043 }4366 }
4367 // If we're configured to reflect the current slide in the
4368 // URL without pushing to history.
4369 else if( config.hash ) {
4370 window.history.replaceState( null, null, '#' + locationHash() );
4371 }
4372 // If history and hash are both disabled, a hash may still
4373 // be added to the URL by clicking on a href with a hash
4374 // target. Counter this by always removing the hash.
4375 else {
4376 window.history.replaceState( null, null, window.location.pathname + window.location.search );
4377 }
4044 }4378 }
40454379
4046 }4380 }
@@ -4108,6 +4442,25 @@
4108 }4442 }
41094443
4110 /**4444 /**
4445 * Returns an array of objects where each object represents the
4446 * attributes on its respective slide.
4447 */
4448 function getSlidesAttributes() {
4449
4450 return getSlides().map( function( slide ) {
4451
4452 var attributes = {};
4453 for( var i = 0; i < slide.attributes.length; i++ ) {
4454 var attribute = slide.attributes[ i ];
4455 attributes[ attribute.name ] = attribute.value;
4456 }
4457 return attributes;
4458
4459 } );
4460
4461 }
4462
4463 /**
4111 * Retrieves the total number of slides in this presentation.4464 * Retrieves the total number of slides in this presentation.
4112 *4465 *
4113 * @return {number}4466 * @return {number}
@@ -4300,6 +4653,73 @@
4300 }4653 }
43014654
4302 /**4655 /**
4656 * Refreshes the fragments on the current slide so that they
4657 * have the appropriate classes (.visible + .current-fragment).
4658 *
4659 * @param {number} [index] The index of the current fragment
4660 * @param {array} [fragments] Array containing all fragments
4661 * in the current slide
4662 *
4663 * @return {{shown: array, hidden: array}}
4664 */
4665 function updateFragments( index, fragments ) {
4666
4667 var changedFragments = {
4668 shown: [],
4669 hidden: []
4670 };
4671
4672 if( currentSlide && config.fragments ) {
4673
4674 fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
4675
4676 if( fragments.length ) {
4677
4678 if( typeof index !== 'number' ) {
4679 var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
4680 if( currentFragment ) {
4681 index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
4682 }
4683 }
4684
4685 toArray( fragments ).forEach( function( el, i ) {
4686
4687 if( el.hasAttribute( 'data-fragment-index' ) ) {
4688 i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
4689 }
4690
4691 // Visible fragments
4692 if( i <= index ) {
4693 if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
4694 el.classList.add( 'visible' );
4695 el.classList.remove( 'current-fragment' );
4696
4697 // Announce the fragments one by one to the Screen Reader
4698 dom.statusDiv.textContent = getStatusText( el );
4699
4700 if( i === index ) {
4701 el.classList.add( 'current-fragment' );
4702 startEmbeddedContent( el );
4703 }
4704 }
4705 // Hidden fragments
4706 else {
4707 if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
4708 el.classList.remove( 'visible' );
4709 el.classList.remove( 'current-fragment' );
4710 }
4711
4712 } );
4713
4714 }
4715
4716 }
4717
4718 return changedFragments;
4719
4720 }
4721
4722 /**
4303 * Navigate to the specified slide fragment.4723 * Navigate to the specified slide fragment.
4304 *4724 *
4305 * @param {?number} index The index of the fragment that4725 * @param {?number} index The index of the fragment that
@@ -4334,53 +4754,24 @@
4334 index += offset;4754 index += offset;
4335 }4755 }
43364756
4337 var fragmentsShown = [],4757 var changedFragments = updateFragments( index, fragments );
4338 fragmentsHidden = [];4758
43394759 if( changedFragments.hidden.length ) {
4340 toArray( fragments ).forEach( function( element, i ) {4760 dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
4341
4342 if( element.hasAttribute( 'data-fragment-index' ) ) {
4343 i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
4344 }
4345
4346 // Visible fragments
4347 if( i <= index ) {
4348 if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
4349 element.classList.add( 'visible' );
4350 element.classList.remove( 'current-fragment' );
4351
4352 // Announce the fragments one by one to the Screen Reader
4353 dom.statusDiv.textContent = getStatusText( element );
4354
4355 if( i === index ) {
4356 element.classList.add( 'current-fragment' );
4357 startEmbeddedContent( element );
4358 }
4359 }
4360 // Hidden fragments
4361 else {
4362 if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
4363 element.classList.remove( 'visible' );
4364 element.classList.remove( 'current-fragment' );
4365 }
4366
4367 } );
4368
4369 if( fragmentsHidden.length ) {
4370 dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
4371 }4761 }
43724762
4373 if( fragmentsShown.length ) {4763 if( changedFragments.shown.length ) {
4374 dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );4764 dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
4375 }4765 }
43764766
4377 updateControls();4767 updateControls();
4378 updateProgress();4768 updateProgress();
4769
4379 if( config.fragmentInURL ) {4770 if( config.fragmentInURL ) {
4380 writeURL();4771 writeURL();
4381 }4772 }
43824773
4383 return !!( fragmentsShown.length || fragmentsHidden.length );4774 return !!( changedFragments.shown.length || changedFragments.hidden.length );
43844775
4385 }4776 }
43864777
@@ -4527,12 +4918,12 @@
4527 // Reverse for RTL4918 // Reverse for RTL
4528 if( config.rtl ) {4919 if( config.rtl ) {
4529 if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {4920 if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
4530 slide( indexh + 1 );4921 slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
4531 }4922 }
4532 }4923 }
4533 // Normal navigation4924 // Normal navigation
4534 else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {4925 else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
4535 slide( indexh - 1 );4926 slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
4536 }4927 }
45374928
4538 }4929 }
@@ -4544,12 +4935,12 @@
4544 // Reverse for RTL4935 // Reverse for RTL
4545 if( config.rtl ) {4936 if( config.rtl ) {
4546 if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {4937 if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
4547 slide( indexh - 1 );4938 slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
4548 }4939 }
4549 }4940 }
4550 // Normal navigation4941 // Normal navigation
4551 else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {4942 else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
4552 slide( indexh + 1 );4943 slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
4553 }4944 }
45544945
4555 }4946 }
@@ -4676,6 +5067,22 @@
4676 }5067 }
46775068
4678 /**5069 /**
5070 * Called whenever there is mouse input at the document level
5071 * to determine if the cursor is active or not.
5072 *
5073 * @param {object} event
5074 */
5075 function onDocumentCursorActive( event ) {
5076
5077 showCursor();
5078
5079 clearTimeout( cursorInactiveTimeout );
5080
5081 cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
5082
5083 }
5084
5085 /**
4679 * Handler for the document level 'keypress' event.5086 * Handler for the document level 'keypress' event.
4680 *5087 *
4681 * @param {object} event5088 * @param {object} event
@@ -4702,20 +5109,31 @@
4702 return true;5109 return true;
4703 }5110 }
47045111
5112 // Shorthand
5113 var keyCode = event.keyCode;
5114
4705 // Remember if auto-sliding was paused so we can toggle it5115 // Remember if auto-sliding was paused so we can toggle it
4706 var autoSlideWasPaused = autoSlidePaused;5116 var autoSlideWasPaused = autoSlidePaused;
47075117
4708 onUserInput( event );5118 onUserInput( event );
47095119
4710 // Check if there's a focused element that could be using5120 // Is there a focused element that could be using the keyboard?
4711 // the keyboard
4712 var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';5121 var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
4713 var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );5122 var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
4714 var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);5123 var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
47155124
5125 // Whitelist specific modified + keycode combinations
5126 var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
5127 var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
5128 var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
5129
5130 // Prevent all other events when a modifier is pressed
5131 var unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
5132 ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
5133
4716 // Disregard the event if there's a focused element or a5134 // Disregard the event if there's a focused element or a
4717 // keyboard modifier key is present5135 // keyboard modifier key is present
4718 if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;5136 if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
47195137
4720 // While paused only allow resume keyboard events; 'b', 'v', '.'5138 // While paused only allow resume keyboard events; 'b', 'v', '.'
4721 var resumeKeyCodes = [66,86,190,191];5139 var resumeKeyCodes = [66,86,190,191];
@@ -4730,7 +5148,7 @@
4730 }5148 }
4731 }5149 }
47325150
4733 if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {5151 if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
4734 return false;5152 return false;
4735 }5153 }
47365154
@@ -4742,7 +5160,7 @@
4742 for( key in config.keyboard ) {5160 for( key in config.keyboard ) {
47435161
4744 // Check if this binding matches the pressed key5162 // Check if this binding matches the pressed key
4745 if( parseInt( key, 10 ) === event.keyCode ) {5163 if( parseInt( key, 10 ) === keyCode ) {
47465164
4747 var value = config.keyboard[ key ];5165 var value = config.keyboard[ key ];
47485166
@@ -4769,7 +5187,7 @@
4769 for( key in registeredKeyBindings ) {5187 for( key in registeredKeyBindings ) {
47705188
4771 // Check if this binding matches the pressed key5189 // Check if this binding matches the pressed key
4772 if( parseInt( key, 10 ) === event.keyCode ) {5190 if( parseInt( key, 10 ) === keyCode ) {
47735191
4774 var action = registeredKeyBindings[ key ].callback;5192 var action = registeredKeyBindings[ key ].callback;
47755193
@@ -4793,35 +5211,92 @@
4793 // Assume true and try to prove false5211 // Assume true and try to prove false
4794 triggered = true;5212 triggered = true;
47955213
4796 switch( event.keyCode ) {5214 // P, PAGE UP
4797 // p, page up5215 if( keyCode === 80 || keyCode === 33 ) {
4798 case 80: case 33: navigatePrev(); break;5216 navigatePrev();
4799 // n, page down5217 }
4800 case 78: case 34: navigateNext(); break;5218 // N, PAGE DOWN
4801 // h, left5219 else if( keyCode === 78 || keyCode === 34 ) {
4802 case 72: case 37: navigateLeft(); break;5220 navigateNext();
4803 // l, right5221 }
4804 case 76: case 39: navigateRight(); break;5222 // H, LEFT
4805 // k, up5223 else if( keyCode === 72 || keyCode === 37 ) {
4806 case 75: case 38: navigateUp(); break;5224 if( firstSlideShortcut ) {
4807 // j, down5225 slide( 0 );
4808 case 74: case 40: navigateDown(); break;5226 }
4809 // home5227 else if( !isOverview() && config.navigationMode === 'linear' ) {
4810 case 36: slide( 0 ); break;5228 navigatePrev();
4811 // end5229 }
4812 case 35: slide( Number.MAX_VALUE ); break;5230 else {
4813 // space5231 navigateLeft();
4814 case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;5232 }
4815 // return5233 }
4816 case 13: isOverview() ? deactivateOverview() : triggered = false; break;5234 // L, RIGHT
4817 // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button5235 else if( keyCode === 76 || keyCode === 39 ) {
4818 case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;5236 if( lastSlideShortcut ) {
4819 // f5237 slide( Number.MAX_VALUE );
4820 case 70: enterFullscreen(); break;5238 }
4821 // a5239 else if( !isOverview() && config.navigationMode === 'linear' ) {
4822 case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;5240 navigateNext();
4823 default:5241 }
4824 triggered = false;5242 else {
5243 navigateRight();
5244 }
5245 }
5246 // K, UP
5247 else if( keyCode === 75 || keyCode === 38 ) {
5248 if( !isOverview() && config.navigationMode === 'linear' ) {
5249 navigatePrev();
5250 }
5251 else {
5252 navigateUp();
5253 }
5254 }
5255 // J, DOWN
5256 else if( keyCode === 74 || keyCode === 40 ) {
5257 if( !isOverview() && config.navigationMode === 'linear' ) {
5258 navigateNext();
5259 }
5260 else {
5261 navigateDown();
5262 }
5263 }
5264 // HOME
5265 else if( keyCode === 36 ) {
5266 slide( 0 );
5267 }
5268 // END
5269 else if( keyCode === 35 ) {
5270 slide( Number.MAX_VALUE );
5271 }
5272 // SPACE
5273 else if( keyCode === 32 ) {
5274 if( isOverview() ) {
5275 deactivateOverview();
5276 }
5277 if( event.shiftKey ) {
5278 navigatePrev();
5279 }
5280 else {
5281 navigateNext();
5282 }
5283 }
5284 // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
5285 else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
5286 togglePause();
5287 }
5288 // F
5289 else if( keyCode === 70 ) {
5290 enterFullscreen();
5291 }
5292 // A
5293 else if( keyCode === 65 ) {
5294 if ( config.autoSlideStoppable ) {
5295 toggleAutoSlide( autoSlideWasPaused );
5296 }
5297 }
5298 else {
5299 triggered = false;
4825 }5300 }
48265301
4827 }5302 }
@@ -4832,7 +5307,7 @@
4832 event.preventDefault && event.preventDefault();5307 event.preventDefault && event.preventDefault();
4833 }5308 }
4834 // ESC or O key5309 // ESC or O key
4835 else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {5310 else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
4836 if( dom.overlay ) {5311 if( dom.overlay ) {
4837 closeOverlay();5312 closeOverlay();
4838 }5313 }
@@ -4863,18 +5338,6 @@
4863 touch.startY = event.touches[0].clientY;5338 touch.startY = event.touches[0].clientY;
4864 touch.startCount = event.touches.length;5339 touch.startCount = event.touches.length;
48655340
4866 // If there's two touches we need to memorize the distance
4867 // between those two points to detect pinching
4868 if( event.touches.length === 2 && config.overview ) {
4869 touch.startSpan = distanceBetween( {
4870 x: event.touches[1].clientX,
4871 y: event.touches[1].clientY
4872 }, {
4873 x: touch.startX,
4874 y: touch.startY
4875 } );
4876 }
4877
4878 }5341 }
48795342
4880 /**5343 /**
@@ -4893,37 +5356,8 @@
4893 var currentX = event.touches[0].clientX;5356 var currentX = event.touches[0].clientX;
4894 var currentY = event.touches[0].clientY;5357 var currentY = event.touches[0].clientY;
48955358
4896 // If the touch started with two points and still has
4897 // two active touches; test for the pinch gesture
4898 if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
4899
4900 // The current distance in pixels between the two touch points
4901 var currentSpan = distanceBetween( {
4902 x: event.touches[1].clientX,
4903 y: event.touches[1].clientY
4904 }, {
4905 x: touch.startX,
4906 y: touch.startY
4907 } );
4908
4909 // If the span is larger than the desire amount we've got
4910 // ourselves a pinch
4911 if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
4912 touch.captured = true;
4913
4914 if( currentSpan < touch.startSpan ) {
4915 activateOverview();
4916 }
4917 else {
4918 deactivateOverview();
4919 }
4920 }
4921
4922 event.preventDefault();
4923
4924 }
4925 // There was only one touch point, look for a swipe5359 // There was only one touch point, look for a swipe
4926 else if( event.touches.length === 1 && touch.startCount !== 2 ) {5360 if( event.touches.length === 1 && touch.startCount !== 2 ) {
49275361
4928 var deltaX = currentX - touch.startX,5362 var deltaX = currentX - touch.startX,
4929 deltaY = currentY - touch.startY;5363 deltaY = currentY - touch.startY;
@@ -5073,8 +5507,8 @@
5073 /**5507 /**
5074 * Event handler for navigation control buttons.5508 * Event handler for navigation control buttons.
5075 */5509 */
5076 function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }5510 function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
5077 function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }5511 function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
5078 function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }5512 function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
5079 function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }5513 function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
5080 function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }5514 function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
@@ -5464,6 +5898,10 @@
5464 // Returns an Array of all slides5898 // Returns an Array of all slides
5465 getSlides: getSlides,5899 getSlides: getSlides,
54665900
5901 // Returns an Array of objects representing the attributes on
5902 // the slides
5903 getSlidesAttributes: getSlidesAttributes,
5904
5467 // Returns the total number of slides5905 // Returns the total number of slides
5468 getTotalSlides: getTotalSlides,5906 getTotalSlides: getTotalSlides,
54695907
@@ -5514,6 +5952,16 @@
5514 return query;5952 return query;
5515 },5953 },
55165954
5955 // Returns the top-level DOM element
5956 getRevealElement: function() {
5957 return dom.wrapper || document.querySelector( '.reveal' );
5958 },
5959
5960 // Returns a hash with all registered plugins
5961 getPlugins: function() {
5962 return plugins;
5963 },
5964
5517 // Returns true if we're currently on the first slide5965 // Returns true if we're currently on the first slide
5518 isFirstSlide: function() {5966 isFirstSlide: function() {
5519 return ( indexh === 0 && indexv === 0 );5967 return ( indexh === 0 && indexv === 0 );
@@ -5555,22 +6003,25 @@
5555 // Forward event binding to the reveal DOM element6003 // Forward event binding to the reveal DOM element
5556 addEventListener: function( type, listener, useCapture ) {6004 addEventListener: function( type, listener, useCapture ) {
5557 if( 'addEventListener' in window ) {6005 if( 'addEventListener' in window ) {
5558 ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );6006 Reveal.getRevealElement().addEventListener( type, listener, useCapture );
5559 }6007 }
5560 },6008 },
5561 removeEventListener: function( type, listener, useCapture ) {6009 removeEventListener: function( type, listener, useCapture ) {
5562 if( 'addEventListener' in window ) {6010 if( 'addEventListener' in window ) {
5563 ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );6011 Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
5564 }6012 }
5565 },6013 },
55666014
5567 // Adds a custom key binding6015 // Adds/removes a custom key binding
5568 addKeyBinding: addKeyBinding,6016 addKeyBinding: addKeyBinding,
5569
5570 // Removes a custom key binding
5571 removeKeyBinding: removeKeyBinding,6017 removeKeyBinding: removeKeyBinding,
55726018
5573 // Programatically triggers a keyboard event6019 // API for registering and retrieving plugins
6020 registerPlugin: registerPlugin,
6021 hasPlugin: hasPlugin,
6022 getPlugin: getPlugin,
6023
6024 // Programmatically triggers a keyboard event
5574 triggerKey: function( keyCode ) {6025 triggerKey: function( keyCode ) {
5575 onDocumentKeyDown( { keyCode: keyCode } );6026 onDocumentKeyDown( { keyCode: keyCode } );
5576 },6027 },