Talk given at Datapalooza Denver 2016 titled "(Your) Data as a Service: The Easy Way to Build an API for Your Data".
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 
 

4745 Zeilen
128 KiB

  1. /*!
  2. * reveal.js
  3. * http://lab.hakim.se/reveal-js
  4. * MIT licensed
  5. *
  6. * Copyright (C) 2016 Hakim El Hattab, http://hakim.se
  7. */
  8. (function( root, factory ) {
  9. if( typeof define === 'function' && define.amd ) {
  10. // AMD. Register as an anonymous module.
  11. define( function() {
  12. root.Reveal = factory();
  13. return root.Reveal;
  14. } );
  15. } else if( typeof exports === 'object' ) {
  16. // Node. Does not work with strict CommonJS.
  17. module.exports = factory();
  18. } else {
  19. // Browser globals.
  20. root.Reveal = factory();
  21. }
  22. }( this, function() {
  23. 'use strict';
  24. var Reveal;
  25. // The reveal.js version
  26. var VERSION = '3.3.0';
  27. var SLIDES_SELECTOR = '.slides section',
  28. HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
  29. VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
  30. HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
  31. UA = navigator.userAgent,
  32. // Configuration defaults, can be overridden at initialization time
  33. config = {
  34. // The "normal" size of the presentation, aspect ratio will be preserved
  35. // when the presentation is scaled to fit different resolutions
  36. width: 960,
  37. height: 700,
  38. // Factor of the display size that should remain empty around the content
  39. margin: 0.1,
  40. // Bounds for smallest/largest possible scale to apply to content
  41. minScale: 0.2,
  42. maxScale: 1.5,
  43. // Display controls in the bottom right corner
  44. controls: true,
  45. // Display a presentation progress bar
  46. progress: true,
  47. // Display the page number of the current slide
  48. slideNumber: false,
  49. // Push each slide change to the browser history
  50. history: false,
  51. // Enable keyboard shortcuts for navigation
  52. keyboard: true,
  53. // Optional function that blocks keyboard events when retuning false
  54. keyboardCondition: null,
  55. // Enable the slide overview mode
  56. overview: true,
  57. // Vertical centering of slides
  58. center: true,
  59. // Enables touch navigation on devices with touch input
  60. touch: true,
  61. // Loop the presentation
  62. loop: false,
  63. // Change the presentation direction to be RTL
  64. rtl: false,
  65. // Randomizes the order of slides each time the presentation loads
  66. shuffle: false,
  67. // Turns fragments on and off globally
  68. fragments: true,
  69. // Flags if the presentation is running in an embedded mode,
  70. // i.e. contained within a limited portion of the screen
  71. embedded: false,
  72. // Flags if we should show a help overlay when the questionmark
  73. // key is pressed
  74. help: true,
  75. // Flags if it should be possible to pause the presentation (blackout)
  76. pause: true,
  77. // Flags if speaker notes should be visible to all viewers
  78. showNotes: false,
  79. // Number of milliseconds between automatically proceeding to the
  80. // next slide, disabled when set to 0, this value can be overwritten
  81. // by using a data-autoslide attribute on your slides
  82. autoSlide: 0,
  83. // Stop auto-sliding after user input
  84. autoSlideStoppable: true,
  85. // Use this method for navigation when auto-sliding (defaults to navigateNext)
  86. autoSlideMethod: null,
  87. // Enable slide navigation via mouse wheel
  88. mouseWheel: false,
  89. // Apply a 3D roll to links on hover
  90. rollingLinks: false,
  91. // Hides the address bar on mobile devices
  92. hideAddressBar: true,
  93. // Opens links in an iframe preview overlay
  94. previewLinks: false,
  95. // Exposes the reveal.js API through window.postMessage
  96. postMessage: true,
  97. // Dispatches all reveal.js events to the parent window through postMessage
  98. postMessageEvents: false,
  99. // Focuses body when page changes visiblity to ensure keyboard shortcuts work
  100. focusBodyOnPageVisibilityChange: true,
  101. // Transition style
  102. transition: 'slide', // none/fade/slide/convex/concave/zoom
  103. // Transition speed
  104. transitionSpeed: 'default', // default/fast/slow
  105. // Transition style for full page slide backgrounds
  106. backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
  107. // Parallax background image
  108. parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
  109. // Parallax background size
  110. parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
  111. // Amount of pixels to move the parallax background per slide step
  112. parallaxBackgroundHorizontal: null,
  113. parallaxBackgroundVertical: null,
  114. // Number of slides away from the current that are visible
  115. viewDistance: 3,
  116. // Script dependencies to load
  117. dependencies: []
  118. },
  119. // Flags if reveal.js is loaded (has dispatched the 'ready' event)
  120. loaded = false,
  121. // Flags if the overview mode is currently active
  122. overview = false,
  123. // Holds the dimensions of our overview slides, including margins
  124. overviewSlideWidth = null,
  125. overviewSlideHeight = null,
  126. // The horizontal and vertical index of the currently active slide
  127. indexh,
  128. indexv,
  129. // The previous and current slide HTML elements
  130. previousSlide,
  131. currentSlide,
  132. previousBackground,
  133. // Slides may hold a data-state attribute which we pick up and apply
  134. // as a class to the body. This list contains the combined state of
  135. // all current slides.
  136. state = [],
  137. // The current scale of the presentation (see width/height config)
  138. scale = 1,
  139. // CSS transform that is currently applied to the slides container,
  140. // split into two groups
  141. slidesTransform = { layout: '', overview: '' },
  142. // Cached references to DOM elements
  143. dom = {},
  144. // Features supported by the browser, see #checkCapabilities()
  145. features = {},
  146. // Client is a mobile device, see #checkCapabilities()
  147. isMobileDevice,
  148. // Client is a desktop Chrome, see #checkCapabilities()
  149. isChrome,
  150. // Throttles mouse wheel navigation
  151. lastMouseWheelStep = 0,
  152. // Delays updates to the URL due to a Chrome thumbnailer bug
  153. writeURLTimeout = 0,
  154. // Flags if the interaction event listeners are bound
  155. eventsAreBound = false,
  156. // The current auto-slide duration
  157. autoSlide = 0,
  158. // Auto slide properties
  159. autoSlidePlayer,
  160. autoSlideTimeout = 0,
  161. autoSlideStartTime = -1,
  162. autoSlidePaused = false,
  163. // Holds information about the currently ongoing touch input
  164. touch = {
  165. startX: 0,
  166. startY: 0,
  167. startSpan: 0,
  168. startCount: 0,
  169. captured: false,
  170. threshold: 40
  171. },
  172. // Holds information about the keyboard shortcuts
  173. keyboardShortcuts = {
  174. 'N , SPACE': 'Next slide',
  175. 'P': 'Previous slide',
  176. '← , H': 'Navigate left',
  177. '→ , L': 'Navigate right',
  178. '↑ , K': 'Navigate up',
  179. '↓ , J': 'Navigate down',
  180. 'Home': 'First slide',
  181. 'End': 'Last slide',
  182. 'B , .': 'Pause',
  183. 'F': 'Fullscreen',
  184. 'ESC, O': 'Slide overview'
  185. };
  186. /**
  187. * Starts up the presentation if the client is capable.
  188. */
  189. function initialize( options ) {
  190. checkCapabilities();
  191. if( !features.transforms2d && !features.transforms3d ) {
  192. document.body.setAttribute( 'class', 'no-transforms' );
  193. // Since JS won't be running any further, we load all lazy
  194. // loading elements upfront
  195. var images = toArray( document.getElementsByTagName( 'img' ) ),
  196. iframes = toArray( document.getElementsByTagName( 'iframe' ) );
  197. var lazyLoadable = images.concat( iframes );
  198. for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
  199. var element = lazyLoadable[i];
  200. if( element.getAttribute( 'data-src' ) ) {
  201. element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
  202. element.removeAttribute( 'data-src' );
  203. }
  204. }
  205. // If the browser doesn't support core features we won't be
  206. // using JavaScript to control the presentation
  207. return;
  208. }
  209. // Cache references to key DOM elements
  210. dom.wrapper = document.querySelector( '.reveal' );
  211. dom.slides = document.querySelector( '.reveal .slides' );
  212. // Force a layout when the whole page, incl fonts, has loaded
  213. window.addEventListener( 'load', layout, false );
  214. var query = Reveal.getQueryHash();
  215. // Do not accept new dependencies via query config to avoid
  216. // the potential of malicious script injection
  217. if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
  218. // Copy options over to our config object
  219. extend( config, options );
  220. extend( config, query );
  221. // Hide the address bar in mobile browsers
  222. hideAddressBar();
  223. // Loads the dependencies and continues to #start() once done
  224. load();
  225. }
  226. /**
  227. * Inspect the client to see what it's capable of, this
  228. * should only happens once per runtime.
  229. */
  230. function checkCapabilities() {
  231. isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
  232. isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
  233. var testElement = document.createElement( 'div' );
  234. features.transforms3d = 'WebkitPerspective' in testElement.style ||
  235. 'MozPerspective' in testElement.style ||
  236. 'msPerspective' in testElement.style ||
  237. 'OPerspective' in testElement.style ||
  238. 'perspective' in testElement.style;
  239. features.transforms2d = 'WebkitTransform' in testElement.style ||
  240. 'MozTransform' in testElement.style ||
  241. 'msTransform' in testElement.style ||
  242. 'OTransform' in testElement.style ||
  243. 'transform' in testElement.style;
  244. features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
  245. features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
  246. features.canvas = !!document.createElement( 'canvas' ).getContext;
  247. // Transitions in the overview are disabled in desktop and
  248. // Safari due to lag
  249. features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
  250. // Flags if we should use zoom instead of transform to scale
  251. // up slides. Zoom produces crisper results but has a lot of
  252. // xbrowser quirks so we only use it in whitelsited browsers.
  253. features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
  254. ( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
  255. }
  256. /**
  257. * Loads the dependencies of reveal.js. Dependencies are
  258. * defined via the configuration option 'dependencies'
  259. * and will be loaded prior to starting/binding reveal.js.
  260. * Some dependencies may have an 'async' flag, if so they
  261. * will load after reveal.js has been started up.
  262. */
  263. function load() {
  264. var scripts = [],
  265. scriptsAsync = [],
  266. scriptsToPreload = 0;
  267. // Called once synchronous scripts finish loading
  268. function proceed() {
  269. if( scriptsAsync.length ) {
  270. // Load asynchronous scripts
  271. head.js.apply( null, scriptsAsync );
  272. }
  273. start();
  274. }
  275. function loadScript( s ) {
  276. head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() {
  277. // Extension may contain callback functions
  278. if( typeof s.callback === 'function' ) {
  279. s.callback.apply( this );
  280. }
  281. if( --scriptsToPreload === 0 ) {
  282. proceed();
  283. }
  284. });
  285. }
  286. for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
  287. var s = config.dependencies[i];
  288. // Load if there's no condition or the condition is truthy
  289. if( !s.condition || s.condition() ) {
  290. if( s.async ) {
  291. scriptsAsync.push( s.src );
  292. }
  293. else {
  294. scripts.push( s.src );
  295. }
  296. loadScript( s );
  297. }
  298. }
  299. if( scripts.length ) {
  300. scriptsToPreload = scripts.length;
  301. // Load synchronous scripts
  302. head.js.apply( null, scripts );
  303. }
  304. else {
  305. proceed();
  306. }
  307. }
  308. /**
  309. * Starts up reveal.js by binding input events and navigating
  310. * to the current URL deeplink if there is one.
  311. */
  312. function start() {
  313. // Make sure we've got all the DOM elements we need
  314. setupDOM();
  315. // Listen to messages posted to this window
  316. setupPostMessage();
  317. // Prevent the slides from being scrolled out of view
  318. setupScrollPrevention();
  319. // Resets all vertical slides so that only the first is visible
  320. resetVerticalSlides();
  321. // Updates the presentation to match the current configuration values
  322. configure();
  323. // Read the initial hash
  324. readURL();
  325. // Update all backgrounds
  326. updateBackground( true );
  327. // Notify listeners that the presentation is ready but use a 1ms
  328. // timeout to ensure it's not fired synchronously after #initialize()
  329. setTimeout( function() {
  330. // Enable transitions now that we're loaded
  331. dom.slides.classList.remove( 'no-transition' );
  332. loaded = true;
  333. dispatchEvent( 'ready', {
  334. 'indexh': indexh,
  335. 'indexv': indexv,
  336. 'currentSlide': currentSlide
  337. } );
  338. }, 1 );
  339. // Special setup and config is required when printing to PDF
  340. if( isPrintingPDF() ) {
  341. removeEventListeners();
  342. // The document needs to have loaded for the PDF layout
  343. // measurements to be accurate
  344. if( document.readyState === 'complete' ) {
  345. setupPDF();
  346. }
  347. else {
  348. window.addEventListener( 'load', setupPDF );
  349. }
  350. }
  351. }
  352. /**
  353. * Finds and stores references to DOM elements which are
  354. * required by the presentation. If a required element is
  355. * not found, it is created.
  356. */
  357. function setupDOM() {
  358. // Prevent transitions while we're loading
  359. dom.slides.classList.add( 'no-transition' );
  360. // Background element
  361. dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
  362. // Progress bar
  363. dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
  364. dom.progressbar = dom.progress.querySelector( 'span' );
  365. // Arrow controls
  366. createSingletonNode( dom.wrapper, 'aside', 'controls',
  367. '<button class="navigate-left" aria-label="previous slide"></button>' +
  368. '<button class="navigate-right" aria-label="next slide"></button>' +
  369. '<button class="navigate-up" aria-label="above slide"></button>' +
  370. '<button class="navigate-down" aria-label="below slide"></button>' );
  371. // Slide number
  372. dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
  373. // Element containing notes that are visible to the audience
  374. dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
  375. dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
  376. // Overlay graphic which is displayed during the paused mode
  377. createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
  378. // Cache references to elements
  379. dom.controls = document.querySelector( '.reveal .controls' );
  380. dom.theme = document.querySelector( '#theme' );
  381. dom.wrapper.setAttribute( 'role', 'application' );
  382. // There can be multiple instances of controls throughout the page
  383. dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
  384. dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
  385. dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
  386. dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
  387. dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
  388. dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
  389. dom.statusDiv = createStatusDiv();
  390. }
  391. /**
  392. * Creates a hidden div with role aria-live to announce the
  393. * current slide content. Hide the div off-screen to make it
  394. * available only to Assistive Technologies.
  395. */
  396. function createStatusDiv() {
  397. var statusDiv = document.getElementById( 'aria-status-div' );
  398. if( !statusDiv ) {
  399. statusDiv = document.createElement( 'div' );
  400. statusDiv.style.position = 'absolute';
  401. statusDiv.style.height = '1px';
  402. statusDiv.style.width = '1px';
  403. statusDiv.style.overflow ='hidden';
  404. statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
  405. statusDiv.setAttribute( 'id', 'aria-status-div' );
  406. statusDiv.setAttribute( 'aria-live', 'polite' );
  407. statusDiv.setAttribute( 'aria-atomic','true' );
  408. dom.wrapper.appendChild( statusDiv );
  409. }
  410. return statusDiv;
  411. }
  412. /**
  413. * Configures the presentation for printing to a static
  414. * PDF.
  415. */
  416. function setupPDF() {
  417. var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
  418. // Dimensions of the PDF pages
  419. var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
  420. pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
  421. // Dimensions of slides within the pages
  422. var slideWidth = slideSize.width,
  423. slideHeight = slideSize.height;
  424. // Let the browser know what page size we want to print
  425. injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
  426. // Limit the size of certain elements to the dimensions of the slide
  427. injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
  428. document.body.classList.add( 'print-pdf' );
  429. document.body.style.width = pageWidth + 'px';
  430. document.body.style.height = pageHeight + 'px';
  431. // Add each slide's index as attributes on itself, we need these
  432. // indices to generate slide numbers below
  433. toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
  434. hslide.setAttribute( 'data-index-h', h );
  435. if( hslide.classList.contains( 'stack' ) ) {
  436. toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
  437. vslide.setAttribute( 'data-index-h', h );
  438. vslide.setAttribute( 'data-index-v', v );
  439. } );
  440. }
  441. } );
  442. // Slide and slide background layout
  443. toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
  444. // Vertical stacks are not centred since their section
  445. // children will be
  446. if( slide.classList.contains( 'stack' ) === false ) {
  447. // Center the slide inside of the page, giving the slide some margin
  448. var left = ( pageWidth - slideWidth ) / 2,
  449. top = ( pageHeight - slideHeight ) / 2;
  450. var contentHeight = getAbsoluteHeight( slide );
  451. var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
  452. // Center slides vertically
  453. if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
  454. top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
  455. }
  456. // Position the slide inside of the page
  457. slide.style.left = left + 'px';
  458. slide.style.top = top + 'px';
  459. slide.style.width = slideWidth + 'px';
  460. // TODO Backgrounds need to be multiplied when the slide
  461. // stretches over multiple pages
  462. var background = slide.querySelector( '.slide-background' );
  463. if( background ) {
  464. background.style.width = pageWidth + 'px';
  465. background.style.height = ( pageHeight * numberOfPages ) + 'px';
  466. background.style.top = -top + 'px';
  467. background.style.left = -left + 'px';
  468. }
  469. // Inject notes if `showNotes` is enabled
  470. if( config.showNotes ) {
  471. var notes = getSlideNotes( slide );
  472. if( notes ) {
  473. var notesSpacing = 8;
  474. var notesElement = document.createElement( 'div' );
  475. notesElement.classList.add( 'speaker-notes' );
  476. notesElement.classList.add( 'speaker-notes-pdf' );
  477. notesElement.innerHTML = notes;
  478. notesElement.style.left = ( notesSpacing - left ) + 'px';
  479. notesElement.style.bottom = ( notesSpacing - top ) + 'px';
  480. notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
  481. slide.appendChild( notesElement );
  482. }
  483. }
  484. // Inject slide numbers if `slideNumbers` are enabled
  485. if( config.slideNumber ) {
  486. var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
  487. slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
  488. var numberElement = document.createElement( 'div' );
  489. numberElement.classList.add( 'slide-number' );
  490. numberElement.classList.add( 'slide-number-pdf' );
  491. numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
  492. background.appendChild( numberElement );
  493. }
  494. }
  495. } );
  496. // Show all fragments
  497. toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) {
  498. fragment.classList.add( 'visible' );
  499. } );
  500. }
  501. /**
  502. * This is an unfortunate necessity. Some actions – such as
  503. * an input field being focused in an iframe or using the
  504. * keyboard to expand text selection beyond the bounds of
  505. * a slide – can trigger our content to be pushed out of view.
  506. * This scrolling can not be prevented by hiding overflow in
  507. * CSS (we already do) so we have to resort to repeatedly
  508. * checking if the slides have been offset :(
  509. */
  510. function setupScrollPrevention() {
  511. setInterval( function() {
  512. if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
  513. dom.wrapper.scrollTop = 0;
  514. dom.wrapper.scrollLeft = 0;
  515. }
  516. }, 1000 );
  517. }
  518. /**
  519. * Creates an HTML element and returns a reference to it.
  520. * If the element already exists the existing instance will
  521. * be returned.
  522. */
  523. function createSingletonNode( container, tagname, classname, innerHTML ) {
  524. // Find all nodes matching the description
  525. var nodes = container.querySelectorAll( '.' + classname );
  526. // Check all matches to find one which is a direct child of
  527. // the specified container
  528. for( var i = 0; i < nodes.length; i++ ) {
  529. var testNode = nodes[i];
  530. if( testNode.parentNode === container ) {
  531. return testNode;
  532. }
  533. }
  534. // If no node was found, create it now
  535. var node = document.createElement( tagname );
  536. node.classList.add( classname );
  537. if( typeof innerHTML === 'string' ) {
  538. node.innerHTML = innerHTML;
  539. }
  540. container.appendChild( node );
  541. return node;
  542. }
  543. /**
  544. * Creates the slide background elements and appends them
  545. * to the background container. One element is created per
  546. * slide no matter if the given slide has visible background.
  547. */
  548. function createBackgrounds() {
  549. var printMode = isPrintingPDF();
  550. // Clear prior backgrounds
  551. dom.background.innerHTML = '';
  552. dom.background.classList.add( 'no-transition' );
  553. // Iterate over all horizontal slides
  554. toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
  555. var backgroundStack;
  556. if( printMode ) {
  557. backgroundStack = createBackground( slideh, slideh );
  558. }
  559. else {
  560. backgroundStack = createBackground( slideh, dom.background );
  561. }
  562. // Iterate over all vertical slides
  563. toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
  564. if( printMode ) {
  565. createBackground( slidev, slidev );
  566. }
  567. else {
  568. createBackground( slidev, backgroundStack );
  569. }
  570. backgroundStack.classList.add( 'stack' );
  571. } );
  572. } );
  573. // Add parallax background if specified
  574. if( config.parallaxBackgroundImage ) {
  575. dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
  576. dom.background.style.backgroundSize = config.parallaxBackgroundSize;
  577. // Make sure the below properties are set on the element - these properties are
  578. // needed for proper transitions to be set on the element via CSS. To remove
  579. // annoying background slide-in effect when the presentation starts, apply
  580. // these properties after short time delay
  581. setTimeout( function() {
  582. dom.wrapper.classList.add( 'has-parallax-background' );
  583. }, 1 );
  584. }
  585. else {
  586. dom.background.style.backgroundImage = '';
  587. dom.wrapper.classList.remove( 'has-parallax-background' );
  588. }
  589. }
  590. /**
  591. * Creates a background for the given slide.
  592. *
  593. * @param {HTMLElement} slide
  594. * @param {HTMLElement} container The element that the background
  595. * should be appended to
  596. */
  597. function createBackground( slide, container ) {
  598. var data = {
  599. background: slide.getAttribute( 'data-background' ),
  600. backgroundSize: slide.getAttribute( 'data-background-size' ),
  601. backgroundImage: slide.getAttribute( 'data-background-image' ),
  602. backgroundVideo: slide.getAttribute( 'data-background-video' ),
  603. backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
  604. backgroundColor: slide.getAttribute( 'data-background-color' ),
  605. backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
  606. backgroundPosition: slide.getAttribute( 'data-background-position' ),
  607. backgroundTransition: slide.getAttribute( 'data-background-transition' )
  608. };
  609. var element = document.createElement( 'div' );
  610. // Carry over custom classes from the slide to the background
  611. element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
  612. if( data.background ) {
  613. // Auto-wrap image urls in url(...)
  614. if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
  615. slide.setAttribute( 'data-background-image', data.background );
  616. }
  617. else {
  618. element.style.background = data.background;
  619. }
  620. }
  621. // Create a hash for this combination of background settings.
  622. // This is used to determine when two slide backgrounds are
  623. // the same.
  624. if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
  625. element.setAttribute( 'data-background-hash', data.background +
  626. data.backgroundSize +
  627. data.backgroundImage +
  628. data.backgroundVideo +
  629. data.backgroundIframe +
  630. data.backgroundColor +
  631. data.backgroundRepeat +
  632. data.backgroundPosition +
  633. data.backgroundTransition );
  634. }
  635. // Additional and optional background properties
  636. if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
  637. if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
  638. if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
  639. if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
  640. if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
  641. container.appendChild( element );
  642. // If backgrounds are being recreated, clear old classes
  643. slide.classList.remove( 'has-dark-background' );
  644. slide.classList.remove( 'has-light-background' );
  645. // If this slide has a background color, add a class that
  646. // signals if it is light or dark. If the slide has no background
  647. // color, no class will be set
  648. var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
  649. if( computedBackgroundColor ) {
  650. var rgb = colorToRgb( computedBackgroundColor );
  651. // Ignore fully transparent backgrounds. Some browsers return
  652. // rgba(0,0,0,0) when reading the computed background color of
  653. // an element with no background
  654. if( rgb && rgb.a !== 0 ) {
  655. if( colorBrightness( computedBackgroundColor ) < 128 ) {
  656. slide.classList.add( 'has-dark-background' );
  657. }
  658. else {
  659. slide.classList.add( 'has-light-background' );
  660. }
  661. }
  662. }
  663. return element;
  664. }
  665. /**
  666. * Registers a listener to postMessage events, this makes it
  667. * possible to call all reveal.js API methods from another
  668. * window. For example:
  669. *
  670. * revealWindow.postMessage( JSON.stringify({
  671. * method: 'slide',
  672. * args: [ 2 ]
  673. * }), '*' );
  674. */
  675. function setupPostMessage() {
  676. if( config.postMessage ) {
  677. window.addEventListener( 'message', function ( event ) {
  678. var data = event.data;
  679. // Make sure we're dealing with JSON
  680. if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
  681. data = JSON.parse( data );
  682. // Check if the requested method can be found
  683. if( data.method && typeof Reveal[data.method] === 'function' ) {
  684. Reveal[data.method].apply( Reveal, data.args );
  685. }
  686. }
  687. }, false );
  688. }
  689. }
  690. /**
  691. * Applies the configuration settings from the config
  692. * object. May be called multiple times.
  693. */
  694. function configure( options ) {
  695. var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
  696. dom.wrapper.classList.remove( config.transition );
  697. // New config options may be passed when this method
  698. // is invoked through the API after initialization
  699. if( typeof options === 'object' ) extend( config, options );
  700. // Force linear transition based on browser capabilities
  701. if( features.transforms3d === false ) config.transition = 'linear';
  702. dom.wrapper.classList.add( config.transition );
  703. dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
  704. dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
  705. dom.controls.style.display = config.controls ? 'block' : 'none';
  706. dom.progress.style.display = config.progress ? 'block' : 'none';
  707. dom.slideNumber.style.display = config.slideNumber && !isPrintingPDF() ? 'block' : 'none';
  708. if( config.shuffle ) {
  709. shuffle();
  710. }
  711. if( config.rtl ) {
  712. dom.wrapper.classList.add( 'rtl' );
  713. }
  714. else {
  715. dom.wrapper.classList.remove( 'rtl' );
  716. }
  717. if( config.center ) {
  718. dom.wrapper.classList.add( 'center' );
  719. }
  720. else {
  721. dom.wrapper.classList.remove( 'center' );
  722. }
  723. // Exit the paused mode if it was configured off
  724. if( config.pause === false ) {
  725. resume();
  726. }
  727. if( config.showNotes ) {
  728. dom.speakerNotes.classList.add( 'visible' );
  729. }
  730. else {
  731. dom.speakerNotes.classList.remove( 'visible' );
  732. }
  733. if( config.mouseWheel ) {
  734. document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
  735. document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
  736. }
  737. else {
  738. document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
  739. document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
  740. }
  741. // Rolling 3D links
  742. if( config.rollingLinks ) {
  743. enableRollingLinks();
  744. }
  745. else {
  746. disableRollingLinks();
  747. }
  748. // Iframe link previews
  749. if( config.previewLinks ) {
  750. enablePreviewLinks();
  751. }
  752. else {
  753. disablePreviewLinks();
  754. enablePreviewLinks( '[data-preview-link]' );
  755. }
  756. // Remove existing auto-slide controls
  757. if( autoSlidePlayer ) {
  758. autoSlidePlayer.destroy();
  759. autoSlidePlayer = null;
  760. }
  761. // Generate auto-slide controls if needed
  762. if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
  763. autoSlidePlayer = new Playback( dom.wrapper, function() {
  764. return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
  765. } );
  766. autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
  767. autoSlidePaused = false;
  768. }
  769. // When fragments are turned off they should be visible
  770. if( config.fragments === false ) {
  771. toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
  772. element.classList.add( 'visible' );
  773. element.classList.remove( 'current-fragment' );
  774. } );
  775. }
  776. sync();
  777. }
  778. /**
  779. * Binds all event listeners.
  780. */
  781. function addEventListeners() {
  782. eventsAreBound = true;
  783. window.addEventListener( 'hashchange', onWindowHashChange, false );
  784. window.addEventListener( 'resize', onWindowResize, false );
  785. if( config.touch ) {
  786. dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
  787. dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
  788. dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
  789. // Support pointer-style touch interaction as well
  790. if( window.navigator.pointerEnabled ) {
  791. // IE 11 uses un-prefixed version of pointer events
  792. dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
  793. dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
  794. dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
  795. }
  796. else if( window.navigator.msPointerEnabled ) {
  797. // IE 10 uses prefixed version of pointer events
  798. dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
  799. dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
  800. dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
  801. }
  802. }
  803. if( config.keyboard ) {
  804. document.addEventListener( 'keydown', onDocumentKeyDown, false );
  805. document.addEventListener( 'keypress', onDocumentKeyPress, false );
  806. }
  807. if( config.progress && dom.progress ) {
  808. dom.progress.addEventListener( 'click', onProgressClicked, false );
  809. }
  810. if( config.focusBodyOnPageVisibilityChange ) {
  811. var visibilityChange;
  812. if( 'hidden' in document ) {
  813. visibilityChange = 'visibilitychange';
  814. }
  815. else if( 'msHidden' in document ) {
  816. visibilityChange = 'msvisibilitychange';
  817. }
  818. else if( 'webkitHidden' in document ) {
  819. visibilityChange = 'webkitvisibilitychange';
  820. }
  821. if( visibilityChange ) {
  822. document.addEventListener( visibilityChange, onPageVisibilityChange, false );
  823. }
  824. }
  825. // Listen to both touch and click events, in case the device
  826. // supports both
  827. var pointerEvents = [ 'touchstart', 'click' ];
  828. // Only support touch for Android, fixes double navigations in
  829. // stock browser
  830. if( UA.match( /android/gi ) ) {
  831. pointerEvents = [ 'touchstart' ];
  832. }
  833. pointerEvents.forEach( function( eventName ) {
  834. dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
  835. dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
  836. dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
  837. dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
  838. dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
  839. dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
  840. } );
  841. }
  842. /**
  843. * Unbinds all event listeners.
  844. */
  845. function removeEventListeners() {
  846. eventsAreBound = false;
  847. document.removeEventListener( 'keydown', onDocumentKeyDown, false );
  848. document.removeEventListener( 'keypress', onDocumentKeyPress, false );
  849. window.removeEventListener( 'hashchange', onWindowHashChange, false );
  850. window.removeEventListener( 'resize', onWindowResize, false );
  851. dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
  852. dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
  853. dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
  854. // IE11
  855. if( window.navigator.pointerEnabled ) {
  856. dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
  857. dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
  858. dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
  859. }
  860. // IE10
  861. else if( window.navigator.msPointerEnabled ) {
  862. dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
  863. dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
  864. dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
  865. }
  866. if ( config.progress && dom.progress ) {
  867. dom.progress.removeEventListener( 'click', onProgressClicked, false );
  868. }
  869. [ 'touchstart', 'click' ].forEach( function( eventName ) {
  870. dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
  871. dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
  872. dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
  873. dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
  874. dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
  875. dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
  876. } );
  877. }
  878. /**
  879. * Extend object a with the properties of object b.
  880. * If there's a conflict, object b takes precedence.
  881. */
  882. function extend( a, b ) {
  883. for( var i in b ) {
  884. a[ i ] = b[ i ];
  885. }
  886. }
  887. /**
  888. * Converts the target object to an array.
  889. */
  890. function toArray( o ) {
  891. return Array.prototype.slice.call( o );
  892. }
  893. /**
  894. * Utility for deserializing a value.
  895. */
  896. function deserialize( value ) {
  897. if( typeof value === 'string' ) {
  898. if( value === 'null' ) return null;
  899. else if( value === 'true' ) return true;
  900. else if( value === 'false' ) return false;
  901. else if( value.match( /^\d+$/ ) ) return parseFloat( value );
  902. }
  903. return value;
  904. }
  905. /**
  906. * Measures the distance in pixels between point a
  907. * and point b.
  908. *
  909. * @param {Object} a point with x/y properties
  910. * @param {Object} b point with x/y properties
  911. */
  912. function distanceBetween( a, b ) {
  913. var dx = a.x - b.x,
  914. dy = a.y - b.y;
  915. return Math.sqrt( dx*dx + dy*dy );
  916. }
  917. /**
  918. * Applies a CSS transform to the target element.
  919. */
  920. function transformElement( element, transform ) {
  921. element.style.WebkitTransform = transform;
  922. element.style.MozTransform = transform;
  923. element.style.msTransform = transform;
  924. element.style.transform = transform;
  925. }
  926. /**
  927. * Applies CSS transforms to the slides container. The container
  928. * is transformed from two separate sources: layout and the overview
  929. * mode.
  930. */
  931. function transformSlides( transforms ) {
  932. // Pick up new transforms from arguments
  933. if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
  934. if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
  935. // Apply the transforms to the slides container
  936. if( slidesTransform.layout ) {
  937. transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
  938. }
  939. else {
  940. transformElement( dom.slides, slidesTransform.overview );
  941. }
  942. }
  943. /**
  944. * Injects the given CSS styles into the DOM.
  945. */
  946. function injectStyleSheet( value ) {
  947. var tag = document.createElement( 'style' );
  948. tag.type = 'text/css';
  949. if( tag.styleSheet ) {
  950. tag.styleSheet.cssText = value;
  951. }
  952. else {
  953. tag.appendChild( document.createTextNode( value ) );
  954. }
  955. document.getElementsByTagName( 'head' )[0].appendChild( tag );
  956. }
  957. /**
  958. * Converts various color input formats to an {r:0,g:0,b:0} object.
  959. *
  960. * @param {String} color The string representation of a color,
  961. * the following formats are supported:
  962. * - #000
  963. * - #000000
  964. * - rgb(0,0,0)
  965. */
  966. function colorToRgb( color ) {
  967. var hex3 = color.match( /^#([0-9a-f]{3})$/i );
  968. if( hex3 && hex3[1] ) {
  969. hex3 = hex3[1];
  970. return {
  971. r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
  972. g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
  973. b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
  974. };
  975. }
  976. var hex6 = color.match( /^#([0-9a-f]{6})$/i );
  977. if( hex6 && hex6[1] ) {
  978. hex6 = hex6[1];
  979. return {
  980. r: parseInt( hex6.substr( 0, 2 ), 16 ),
  981. g: parseInt( hex6.substr( 2, 2 ), 16 ),
  982. b: parseInt( hex6.substr( 4, 2 ), 16 )
  983. };
  984. }
  985. var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
  986. if( rgb ) {
  987. return {
  988. r: parseInt( rgb[1], 10 ),
  989. g: parseInt( rgb[2], 10 ),
  990. b: parseInt( rgb[3], 10 )
  991. };
  992. }
  993. var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
  994. if( rgba ) {
  995. return {
  996. r: parseInt( rgba[1], 10 ),
  997. g: parseInt( rgba[2], 10 ),
  998. b: parseInt( rgba[3], 10 ),
  999. a: parseFloat( rgba[4] )
  1000. };
  1001. }
  1002. return null;
  1003. }
  1004. /**
  1005. * Calculates brightness on a scale of 0-255.
  1006. *
  1007. * @param color See colorStringToRgb for supported formats.
  1008. */
  1009. function colorBrightness( color ) {
  1010. if( typeof color === 'string' ) color = colorToRgb( color );
  1011. if( color ) {
  1012. return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
  1013. }
  1014. return null;
  1015. }
  1016. /**
  1017. * Retrieves the height of the given element by looking
  1018. * at the position and height of its immediate children.
  1019. */
  1020. function getAbsoluteHeight( element ) {
  1021. var height = 0;
  1022. if( element ) {
  1023. var absoluteChildren = 0;
  1024. toArray( element.childNodes ).forEach( function( child ) {
  1025. if( typeof child.offsetTop === 'number' && child.style ) {
  1026. // Count # of abs children
  1027. if( window.getComputedStyle( child ).position === 'absolute' ) {
  1028. absoluteChildren += 1;
  1029. }
  1030. height = Math.max( height, child.offsetTop + child.offsetHeight );
  1031. }
  1032. } );
  1033. // If there are no absolute children, use offsetHeight
  1034. if( absoluteChildren === 0 ) {
  1035. height = element.offsetHeight;
  1036. }
  1037. }
  1038. return height;
  1039. }
  1040. /**
  1041. * Returns the remaining height within the parent of the
  1042. * target element.
  1043. *
  1044. * remaining height = [ configured parent height ] - [ current parent height ]
  1045. */
  1046. function getRemainingHeight( element, height ) {
  1047. height = height || 0;
  1048. if( element ) {
  1049. var newHeight, oldHeight = element.style.height;
  1050. // Change the .stretch element height to 0 in order find the height of all
  1051. // the other elements
  1052. element.style.height = '0px';
  1053. newHeight = height - element.parentNode.offsetHeight;
  1054. // Restore the old height, just in case
  1055. element.style.height = oldHeight + 'px';
  1056. return newHeight;
  1057. }
  1058. return height;
  1059. }
  1060. /**
  1061. * Checks if this instance is being used to print a PDF.
  1062. */
  1063. function isPrintingPDF() {
  1064. return ( /print-pdf/gi ).test( window.location.search );
  1065. }
  1066. /**
  1067. * Hides the address bar if we're on a mobile device.
  1068. */
  1069. function hideAddressBar() {
  1070. if( config.hideAddressBar && isMobileDevice ) {
  1071. // Events that should trigger the address bar to hide
  1072. window.addEventListener( 'load', removeAddressBar, false );
  1073. window.addEventListener( 'orientationchange', removeAddressBar, false );
  1074. }
  1075. }
  1076. /**
  1077. * Causes the address bar to hide on mobile devices,
  1078. * more vertical space ftw.
  1079. */
  1080. function removeAddressBar() {
  1081. setTimeout( function() {
  1082. window.scrollTo( 0, 1 );
  1083. }, 10 );
  1084. }
  1085. /**
  1086. * Dispatches an event of the specified type from the
  1087. * reveal DOM element.
  1088. */
  1089. function dispatchEvent( type, args ) {
  1090. var event = document.createEvent( 'HTMLEvents', 1, 2 );
  1091. event.initEvent( type, true, true );
  1092. extend( event, args );
  1093. dom.wrapper.dispatchEvent( event );
  1094. // If we're in an iframe, post each reveal.js event to the
  1095. // parent window. Used by the notes plugin
  1096. if( config.postMessageEvents && window.parent !== window.self ) {
  1097. window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
  1098. }
  1099. }
  1100. /**
  1101. * Wrap all links in 3D goodness.
  1102. */
  1103. function enableRollingLinks() {
  1104. if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
  1105. var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
  1106. for( var i = 0, len = anchors.length; i < len; i++ ) {
  1107. var anchor = anchors[i];
  1108. if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
  1109. var span = document.createElement('span');
  1110. span.setAttribute('data-title', anchor.text);
  1111. span.innerHTML = anchor.innerHTML;
  1112. anchor.classList.add( 'roll' );
  1113. anchor.innerHTML = '';
  1114. anchor.appendChild(span);
  1115. }
  1116. }
  1117. }
  1118. }
  1119. /**
  1120. * Unwrap all 3D links.
  1121. */
  1122. function disableRollingLinks() {
  1123. var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
  1124. for( var i = 0, len = anchors.length; i < len; i++ ) {
  1125. var anchor = anchors[i];
  1126. var span = anchor.querySelector( 'span' );
  1127. if( span ) {
  1128. anchor.classList.remove( 'roll' );
  1129. anchor.innerHTML = span.innerHTML;
  1130. }
  1131. }
  1132. }
  1133. /**
  1134. * Bind preview frame links.
  1135. */
  1136. function enablePreviewLinks( selector ) {
  1137. var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
  1138. anchors.forEach( function( element ) {
  1139. if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
  1140. element.addEventListener( 'click', onPreviewLinkClicked, false );
  1141. }
  1142. } );
  1143. }
  1144. /**
  1145. * Unbind preview frame links.
  1146. */
  1147. function disablePreviewLinks() {
  1148. var anchors = toArray( document.querySelectorAll( 'a' ) );
  1149. anchors.forEach( function( element ) {
  1150. if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
  1151. element.removeEventListener( 'click', onPreviewLinkClicked, false );
  1152. }
  1153. } );
  1154. }
  1155. /**
  1156. * Opens a preview window for the target URL.
  1157. */
  1158. function showPreview( url ) {
  1159. closeOverlay();
  1160. dom.overlay = document.createElement( 'div' );
  1161. dom.overlay.classList.add( 'overlay' );
  1162. dom.overlay.classList.add( 'overlay-preview' );
  1163. dom.wrapper.appendChild( dom.overlay );
  1164. dom.overlay.innerHTML = [
  1165. '<header>',
  1166. '<a class="close" href="#"><span class="icon"></span></a>',
  1167. '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
  1168. '</header>',
  1169. '<div class="spinner"></div>',
  1170. '<div class="viewport">',
  1171. '<iframe src="'+ url +'"></iframe>',
  1172. '</div>'
  1173. ].join('');
  1174. dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
  1175. dom.overlay.classList.add( 'loaded' );
  1176. }, false );
  1177. dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
  1178. closeOverlay();
  1179. event.preventDefault();
  1180. }, false );
  1181. dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
  1182. closeOverlay();
  1183. }, false );
  1184. setTimeout( function() {
  1185. dom.overlay.classList.add( 'visible' );
  1186. }, 1 );
  1187. }
  1188. /**
  1189. * Opens a overlay window with help material.
  1190. */
  1191. function showHelp() {
  1192. if( config.help ) {
  1193. closeOverlay();
  1194. dom.overlay = document.createElement( 'div' );
  1195. dom.overlay.classList.add( 'overlay' );
  1196. dom.overlay.classList.add( 'overlay-help' );
  1197. dom.wrapper.appendChild( dom.overlay );
  1198. var html = '<p class="title">Keyboard Shortcuts</p><br/>';
  1199. html += '<table><th>KEY</th><th>ACTION</th>';
  1200. for( var key in keyboardShortcuts ) {
  1201. html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
  1202. }
  1203. html += '</table>';
  1204. dom.overlay.innerHTML = [
  1205. '<header>',
  1206. '<a class="close" href="#"><span class="icon"></span></a>',
  1207. '</header>',
  1208. '<div class="viewport">',
  1209. '<div class="viewport-inner">'+ html +'</div>',
  1210. '</div>'
  1211. ].join('');
  1212. dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
  1213. closeOverlay();
  1214. event.preventDefault();
  1215. }, false );
  1216. setTimeout( function() {
  1217. dom.overlay.classList.add( 'visible' );
  1218. }, 1 );
  1219. }
  1220. }
  1221. /**
  1222. * Closes any currently open overlay.
  1223. */
  1224. function closeOverlay() {
  1225. if( dom.overlay ) {
  1226. dom.overlay.parentNode.removeChild( dom.overlay );
  1227. dom.overlay = null;
  1228. }
  1229. }
  1230. /**
  1231. * Applies JavaScript-controlled layout rules to the
  1232. * presentation.
  1233. */
  1234. function layout() {
  1235. if( dom.wrapper && !isPrintingPDF() ) {
  1236. var size = getComputedSlideSize();
  1237. var slidePadding = 20; // TODO Dig this out of DOM
  1238. // Layout the contents of the slides
  1239. layoutSlideContents( config.width, config.height, slidePadding );
  1240. dom.slides.style.width = size.width + 'px';
  1241. dom.slides.style.height = size.height + 'px';
  1242. // Determine scale of content to fit within available space
  1243. scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
  1244. // Respect max/min scale settings
  1245. scale = Math.max( scale, config.minScale );
  1246. scale = Math.min( scale, config.maxScale );
  1247. // Don't apply any scaling styles if scale is 1
  1248. if( scale === 1 ) {
  1249. dom.slides.style.zoom = '';
  1250. dom.slides.style.left = '';
  1251. dom.slides.style.top = '';
  1252. dom.slides.style.bottom = '';
  1253. dom.slides.style.right = '';
  1254. transformSlides( { layout: '' } );
  1255. }
  1256. else {
  1257. // Prefer zoom for scaling up so that content remains crisp.
  1258. // Don't use zoom to scale down since that can lead to shifts
  1259. // in text layout/line breaks.
  1260. if( scale > 1 && features.zoom ) {
  1261. dom.slides.style.zoom = scale;
  1262. dom.slides.style.left = '';
  1263. dom.slides.style.top = '';
  1264. dom.slides.style.bottom = '';
  1265. dom.slides.style.right = '';
  1266. transformSlides( { layout: '' } );
  1267. }
  1268. // Apply scale transform as a fallback
  1269. else {
  1270. dom.slides.style.zoom = '';
  1271. dom.slides.style.left = '50%';
  1272. dom.slides.style.top = '50%';
  1273. dom.slides.style.bottom = 'auto';
  1274. dom.slides.style.right = 'auto';
  1275. transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
  1276. }
  1277. }
  1278. // Select all slides, vertical and horizontal
  1279. var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
  1280. for( var i = 0, len = slides.length; i < len; i++ ) {
  1281. var slide = slides[ i ];
  1282. // Don't bother updating invisible slides
  1283. if( slide.style.display === 'none' ) {
  1284. continue;
  1285. }
  1286. if( config.center || slide.classList.contains( 'center' ) ) {
  1287. // Vertical stacks are not centred since their section
  1288. // children will be
  1289. if( slide.classList.contains( 'stack' ) ) {
  1290. slide.style.top = 0;
  1291. }
  1292. else {
  1293. slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px';
  1294. }
  1295. }
  1296. else {
  1297. slide.style.top = '';
  1298. }
  1299. }
  1300. updateProgress();
  1301. updateParallax();
  1302. }
  1303. }
  1304. /**
  1305. * Applies layout logic to the contents of all slides in
  1306. * the presentation.
  1307. */
  1308. function layoutSlideContents( width, height, padding ) {
  1309. // Handle sizing of elements with the 'stretch' class
  1310. toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
  1311. // Determine how much vertical space we can use
  1312. var remainingHeight = getRemainingHeight( element, height );
  1313. // Consider the aspect ratio of media elements
  1314. if( /(img|video)/gi.test( element.nodeName ) ) {
  1315. var nw = element.naturalWidth || element.videoWidth,
  1316. nh = element.naturalHeight || element.videoHeight;
  1317. var es = Math.min( width / nw, remainingHeight / nh );
  1318. element.style.width = ( nw * es ) + 'px';
  1319. element.style.height = ( nh * es ) + 'px';
  1320. }
  1321. else {
  1322. element.style.width = width + 'px';
  1323. element.style.height = remainingHeight + 'px';
  1324. }
  1325. } );
  1326. }
  1327. /**
  1328. * Calculates the computed pixel size of our slides. These
  1329. * values are based on the width and height configuration
  1330. * options.
  1331. */
  1332. function getComputedSlideSize( presentationWidth, presentationHeight ) {
  1333. var size = {
  1334. // Slide size
  1335. width: config.width,
  1336. height: config.height,
  1337. // Presentation size
  1338. presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
  1339. presentationHeight: presentationHeight || dom.wrapper.offsetHeight
  1340. };
  1341. // Reduce available space by margin
  1342. size.presentationWidth -= ( size.presentationWidth * config.margin );
  1343. size.presentationHeight -= ( size.presentationHeight * config.margin );
  1344. // Slide width may be a percentage of available width
  1345. if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
  1346. size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
  1347. }
  1348. // Slide height may be a percentage of available height
  1349. if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
  1350. size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
  1351. }
  1352. return size;
  1353. }
  1354. /**
  1355. * Stores the vertical index of a stack so that the same
  1356. * vertical slide can be selected when navigating to and
  1357. * from the stack.
  1358. *
  1359. * @param {HTMLElement} stack The vertical stack element
  1360. * @param {int} v Index to memorize
  1361. */
  1362. function setPreviousVerticalIndex( stack, v ) {
  1363. if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
  1364. stack.setAttribute( 'data-previous-indexv', v || 0 );
  1365. }
  1366. }
  1367. /**
  1368. * Retrieves the vertical index which was stored using
  1369. * #setPreviousVerticalIndex() or 0 if no previous index
  1370. * exists.
  1371. *
  1372. * @param {HTMLElement} stack The vertical stack element
  1373. */
  1374. function getPreviousVerticalIndex( stack ) {
  1375. if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
  1376. // Prefer manually defined start-indexv
  1377. var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
  1378. return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
  1379. }
  1380. return 0;
  1381. }
  1382. /**
  1383. * Displays the overview of slides (quick nav) by scaling
  1384. * down and arranging all slide elements.
  1385. */
  1386. function activateOverview() {
  1387. // Only proceed if enabled in config
  1388. if( config.overview && !isOverview() ) {
  1389. overview = true;
  1390. dom.wrapper.classList.add( 'overview' );
  1391. dom.wrapper.classList.remove( 'overview-deactivating' );
  1392. if( features.overviewTransitions ) {
  1393. setTimeout( function() {
  1394. dom.wrapper.classList.add( 'overview-animated' );
  1395. }, 1 );
  1396. }
  1397. // Don't auto-slide while in overview mode
  1398. cancelAutoSlide();
  1399. // Move the backgrounds element into the slide container to
  1400. // that the same scaling is applied
  1401. dom.slides.appendChild( dom.background );
  1402. // Clicking on an overview slide navigates to it
  1403. toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
  1404. if( !slide.classList.contains( 'stack' ) ) {
  1405. slide.addEventListener( 'click', onOverviewSlideClicked, true );
  1406. }
  1407. } );
  1408. // Calculate slide sizes
  1409. var margin = 70;
  1410. var slideSize = getComputedSlideSize();
  1411. overviewSlideWidth = slideSize.width + margin;
  1412. overviewSlideHeight = slideSize.height + margin;
  1413. // Reverse in RTL mode
  1414. if( config.rtl ) {
  1415. overviewSlideWidth = -overviewSlideWidth;
  1416. }
  1417. updateSlidesVisibility();
  1418. layoutOverview();
  1419. updateOverview();
  1420. layout();
  1421. // Notify observers of the overview showing
  1422. dispatchEvent( 'overviewshown', {
  1423. 'indexh': indexh,
  1424. 'indexv': indexv,
  1425. 'currentSlide': currentSlide
  1426. } );
  1427. }
  1428. }
  1429. /**
  1430. * Uses CSS transforms to position all slides in a grid for
  1431. * display inside of the overview mode.
  1432. */
  1433. function layoutOverview() {
  1434. // Layout slides
  1435. toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
  1436. hslide.setAttribute( 'data-index-h', h );
  1437. transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
  1438. if( hslide.classList.contains( 'stack' ) ) {
  1439. toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
  1440. vslide.setAttribute( 'data-index-h', h );
  1441. vslide.setAttribute( 'data-index-v', v );
  1442. transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
  1443. } );
  1444. }
  1445. } );
  1446. // Layout slide backgrounds
  1447. toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
  1448. transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
  1449. toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
  1450. transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
  1451. } );
  1452. } );
  1453. }
  1454. /**
  1455. * Moves the overview viewport to the current slides.
  1456. * Called each time the current slide changes.
  1457. */
  1458. function updateOverview() {
  1459. transformSlides( {
  1460. overview: [
  1461. 'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
  1462. 'translateY('+ ( -indexv * overviewSlideHeight ) +'px)',
  1463. 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
  1464. ].join( ' ' )
  1465. } );
  1466. }
  1467. /**
  1468. * Exits the slide overview and enters the currently
  1469. * active slide.
  1470. */
  1471. function deactivateOverview() {
  1472. // Only proceed if enabled in config
  1473. if( config.overview ) {
  1474. overview = false;
  1475. dom.wrapper.classList.remove( 'overview' );
  1476. dom.wrapper.classList.remove( 'overview-animated' );
  1477. // Temporarily add a class so that transitions can do different things
  1478. // depending on whether they are exiting/entering overview, or just
  1479. // moving from slide to slide
  1480. dom.wrapper.classList.add( 'overview-deactivating' );
  1481. setTimeout( function () {
  1482. dom.wrapper.classList.remove( 'overview-deactivating' );
  1483. }, 1 );
  1484. // Move the background element back out
  1485. dom.wrapper.appendChild( dom.background );
  1486. // Clean up changes made to slides
  1487. toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
  1488. transformElement( slide, '' );
  1489. slide.removeEventListener( 'click', onOverviewSlideClicked, true );
  1490. } );
  1491. // Clean up changes made to backgrounds
  1492. toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
  1493. transformElement( background, '' );
  1494. } );
  1495. transformSlides( { overview: '' } );
  1496. slide( indexh, indexv );
  1497. layout();
  1498. cueAutoSlide();
  1499. // Notify observers of the overview hiding
  1500. dispatchEvent( 'overviewhidden', {
  1501. 'indexh': indexh,
  1502. 'indexv': indexv,
  1503. 'currentSlide': currentSlide
  1504. } );
  1505. }
  1506. }
  1507. /**
  1508. * Toggles the slide overview mode on and off.
  1509. *
  1510. * @param {Boolean} override Optional flag which overrides the
  1511. * toggle logic and forcibly sets the desired state. True means
  1512. * overview is open, false means it's closed.
  1513. */
  1514. function toggleOverview( override ) {
  1515. if( typeof override === 'boolean' ) {
  1516. override ? activateOverview() : deactivateOverview();
  1517. }
  1518. else {
  1519. isOverview() ? deactivateOverview() : activateOverview();
  1520. }
  1521. }
  1522. /**
  1523. * Checks if the overview is currently active.
  1524. *
  1525. * @return {Boolean} true if the overview is active,
  1526. * false otherwise
  1527. */
  1528. function isOverview() {
  1529. return overview;
  1530. }
  1531. /**
  1532. * Checks if the current or specified slide is vertical
  1533. * (nested within another slide).
  1534. *
  1535. * @param {HTMLElement} slide [optional] The slide to check
  1536. * orientation of
  1537. */
  1538. function isVerticalSlide( slide ) {
  1539. // Prefer slide argument, otherwise use current slide
  1540. slide = slide ? slide : currentSlide;
  1541. return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
  1542. }
  1543. /**
  1544. * Handling the fullscreen functionality via the fullscreen API
  1545. *
  1546. * @see http://fullscreen.spec.whatwg.org/
  1547. * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
  1548. */
  1549. function enterFullscreen() {
  1550. var element = document.body;
  1551. // Check which implementation is available
  1552. var requestMethod = element.requestFullScreen ||
  1553. element.webkitRequestFullscreen ||
  1554. element.webkitRequestFullScreen ||
  1555. element.mozRequestFullScreen ||
  1556. element.msRequestFullscreen;
  1557. if( requestMethod ) {
  1558. requestMethod.apply( element );
  1559. }
  1560. }
  1561. /**
  1562. * Enters the paused mode which fades everything on screen to
  1563. * black.
  1564. */
  1565. function pause() {
  1566. if( config.pause ) {
  1567. var wasPaused = dom.wrapper.classList.contains( 'paused' );
  1568. cancelAutoSlide();
  1569. dom.wrapper.classList.add( 'paused' );
  1570. if( wasPaused === false ) {
  1571. dispatchEvent( 'paused' );
  1572. }
  1573. }
  1574. }
  1575. /**
  1576. * Exits from the paused mode.
  1577. */
  1578. function resume() {
  1579. var wasPaused = dom.wrapper.classList.contains( 'paused' );
  1580. dom.wrapper.classList.remove( 'paused' );
  1581. cueAutoSlide();
  1582. if( wasPaused ) {
  1583. dispatchEvent( 'resumed' );
  1584. }
  1585. }
  1586. /**
  1587. * Toggles the paused mode on and off.
  1588. */
  1589. function togglePause( override ) {
  1590. if( typeof override === 'boolean' ) {
  1591. override ? pause() : resume();
  1592. }
  1593. else {
  1594. isPaused() ? resume() : pause();
  1595. }
  1596. }
  1597. /**
  1598. * Checks if we are currently in the paused mode.
  1599. */
  1600. function isPaused() {
  1601. return dom.wrapper.classList.contains( 'paused' );
  1602. }
  1603. /**
  1604. * Toggles the auto slide mode on and off.
  1605. *
  1606. * @param {Boolean} override Optional flag which sets the desired state.
  1607. * True means autoplay starts, false means it stops.
  1608. */
  1609. function toggleAutoSlide( override ) {
  1610. if( typeof override === 'boolean' ) {
  1611. override ? resumeAutoSlide() : pauseAutoSlide();
  1612. }
  1613. else {
  1614. autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
  1615. }
  1616. }
  1617. /**
  1618. * Checks if the auto slide mode is currently on.
  1619. */
  1620. function isAutoSliding() {
  1621. return !!( autoSlide && !autoSlidePaused );
  1622. }
  1623. /**
  1624. * Steps from the current point in the presentation to the
  1625. * slide which matches the specified horizontal and vertical
  1626. * indices.
  1627. *
  1628. * @param {int} h Horizontal index of the target slide
  1629. * @param {int} v Vertical index of the target slide
  1630. * @param {int} f Optional index of a fragment within the
  1631. * target slide to activate
  1632. * @param {int} o Optional origin for use in multimaster environments
  1633. */
  1634. function slide( h, v, f, o ) {
  1635. // Remember where we were at before
  1636. previousSlide = currentSlide;
  1637. // Query all horizontal slides in the deck
  1638. var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
  1639. // If no vertical index is specified and the upcoming slide is a
  1640. // stack, resume at its previous vertical index
  1641. if( v === undefined && !isOverview() ) {
  1642. v = getPreviousVerticalIndex( horizontalSlides[ h ] );
  1643. }
  1644. // If we were on a vertical stack, remember what vertical index
  1645. // it was on so we can resume at the same position when returning
  1646. if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
  1647. setPreviousVerticalIndex( previousSlide.parentNode, indexv );
  1648. }
  1649. // Remember the state before this slide
  1650. var stateBefore = state.concat();
  1651. // Reset the state array
  1652. state.length = 0;
  1653. var indexhBefore = indexh || 0,
  1654. indexvBefore = indexv || 0;
  1655. // Activate and transition to the new slide
  1656. indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
  1657. indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
  1658. // Update the visibility of slides now that the indices have changed
  1659. updateSlidesVisibility();
  1660. layout();
  1661. // Apply the new state
  1662. stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
  1663. // Check if this state existed on the previous slide. If it
  1664. // did, we will avoid adding it repeatedly
  1665. for( var j = 0; j < stateBefore.length; j++ ) {
  1666. if( stateBefore[j] === state[i] ) {
  1667. stateBefore.splice( j, 1 );
  1668. continue stateLoop;
  1669. }
  1670. }
  1671. document.documentElement.classList.add( state[i] );
  1672. // Dispatch custom event matching the state's name
  1673. dispatchEvent( state[i] );
  1674. }
  1675. // Clean up the remains of the previous state
  1676. while( stateBefore.length ) {
  1677. document.documentElement.classList.remove( stateBefore.pop() );
  1678. }
  1679. // Update the overview if it's currently active
  1680. if( isOverview() ) {
  1681. updateOverview();
  1682. }
  1683. // Find the current horizontal slide and any possible vertical slides
  1684. // within it
  1685. var currentHorizontalSlide = horizontalSlides[ indexh ],
  1686. currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
  1687. // Store references to the previous and current slides
  1688. currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
  1689. // Show fragment, if specified
  1690. if( typeof f !== 'undefined' ) {
  1691. navigateFragment( f );
  1692. }
  1693. // Dispatch an event if the slide changed
  1694. var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
  1695. if( slideChanged ) {
  1696. dispatchEvent( 'slidechanged', {
  1697. 'indexh': indexh,
  1698. 'indexv': indexv,
  1699. 'previousSlide': previousSlide,
  1700. 'currentSlide': currentSlide,
  1701. 'origin': o
  1702. } );
  1703. }
  1704. else {
  1705. // Ensure that the previous slide is never the same as the current
  1706. previousSlide = null;
  1707. }
  1708. // Solves an edge case where the previous slide maintains the
  1709. // 'present' class when navigating between adjacent vertical
  1710. // stacks
  1711. if( previousSlide ) {
  1712. previousSlide.classList.remove( 'present' );
  1713. previousSlide.setAttribute( 'aria-hidden', 'true' );
  1714. // Reset all slides upon navigate to home
  1715. // Issue: #285
  1716. if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
  1717. // Launch async task
  1718. setTimeout( function () {
  1719. var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
  1720. for( i in slides ) {
  1721. if( slides[i] ) {
  1722. // Reset stack
  1723. setPreviousVerticalIndex( slides[i], 0 );
  1724. }
  1725. }
  1726. }, 0 );
  1727. }
  1728. }
  1729. // Handle embedded content
  1730. if( slideChanged || !previousSlide ) {
  1731. stopEmbeddedContent( previousSlide );
  1732. startEmbeddedContent( currentSlide );
  1733. }
  1734. // Announce the current slide contents, for screen readers
  1735. dom.statusDiv.textContent = currentSlide.textContent;
  1736. updateControls();
  1737. updateProgress();
  1738. updateBackground();
  1739. updateParallax();
  1740. updateSlideNumber();
  1741. updateNotes();
  1742. // Update the URL hash
  1743. writeURL();
  1744. cueAutoSlide();
  1745. }
  1746. /**
  1747. * Syncs the presentation with the current DOM. Useful
  1748. * when new slides or control elements are added or when
  1749. * the configuration has changed.
  1750. */
  1751. function sync() {
  1752. // Subscribe to input
  1753. removeEventListeners();
  1754. addEventListeners();
  1755. // Force a layout to make sure the current config is accounted for
  1756. layout();
  1757. // Reflect the current autoSlide value
  1758. autoSlide = config.autoSlide;
  1759. // Start auto-sliding if it's enabled
  1760. cueAutoSlide();
  1761. // Re-create the slide backgrounds
  1762. createBackgrounds();
  1763. // Write the current hash to the URL
  1764. writeURL();
  1765. sortAllFragments();
  1766. updateControls();
  1767. updateProgress();
  1768. updateBackground( true );
  1769. updateSlideNumber();
  1770. updateSlidesVisibility();
  1771. updateNotes();
  1772. formatEmbeddedContent();
  1773. startEmbeddedContent( currentSlide );
  1774. if( isOverview() ) {
  1775. layoutOverview();
  1776. }
  1777. }
  1778. /**
  1779. * Resets all vertical slides so that only the first
  1780. * is visible.
  1781. */
  1782. function resetVerticalSlides() {
  1783. var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
  1784. horizontalSlides.forEach( function( horizontalSlide ) {
  1785. var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
  1786. verticalSlides.forEach( function( verticalSlide, y ) {
  1787. if( y > 0 ) {
  1788. verticalSlide.classList.remove( 'present' );
  1789. verticalSlide.classList.remove( 'past' );
  1790. verticalSlide.classList.add( 'future' );
  1791. verticalSlide.setAttribute( 'aria-hidden', 'true' );
  1792. }
  1793. } );
  1794. } );
  1795. }
  1796. /**
  1797. * Sorts and formats all of fragments in the
  1798. * presentation.
  1799. */
  1800. function sortAllFragments() {
  1801. var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
  1802. horizontalSlides.forEach( function( horizontalSlide ) {
  1803. var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
  1804. verticalSlides.forEach( function( verticalSlide, y ) {
  1805. sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
  1806. } );
  1807. if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
  1808. } );
  1809. }
  1810. /**
  1811. * Randomly shuffles all slides in the deck.
  1812. */
  1813. function shuffle() {
  1814. var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
  1815. slides.forEach( function( slide ) {
  1816. // Insert this slide next to another random slide. This may
  1817. // cause the slide to insert before itself but that's fine.
  1818. dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
  1819. } );
  1820. }
  1821. /**
  1822. * Updates one dimension of slides by showing the slide
  1823. * with the specified index.
  1824. *
  1825. * @param {String} selector A CSS selector that will fetch
  1826. * the group of slides we are working with
  1827. * @param {Number} index The index of the slide that should be
  1828. * shown
  1829. *
  1830. * @return {Number} The index of the slide that is now shown,
  1831. * might differ from the passed in index if it was out of
  1832. * bounds.
  1833. */
  1834. function updateSlides( selector, index ) {
  1835. // Select all slides and convert the NodeList result to
  1836. // an array
  1837. var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
  1838. slidesLength = slides.length;
  1839. var printMode = isPrintingPDF();
  1840. if( slidesLength ) {
  1841. // Should the index loop?
  1842. if( config.loop ) {
  1843. index %= slidesLength;
  1844. if( index < 0 ) {
  1845. index = slidesLength + index;
  1846. }
  1847. }
  1848. // Enforce max and minimum index bounds
  1849. index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
  1850. for( var i = 0; i < slidesLength; i++ ) {
  1851. var element = slides[i];
  1852. var reverse = config.rtl && !isVerticalSlide( element );
  1853. element.classList.remove( 'past' );
  1854. element.classList.remove( 'present' );
  1855. element.classList.remove( 'future' );
  1856. // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
  1857. element.setAttribute( 'hidden', '' );
  1858. element.setAttribute( 'aria-hidden', 'true' );
  1859. // If this element contains vertical slides
  1860. if( element.querySelector( 'section' ) ) {
  1861. element.classList.add( 'stack' );
  1862. }
  1863. // If we're printing static slides, all slides are "present"
  1864. if( printMode ) {
  1865. element.classList.add( 'present' );
  1866. continue;
  1867. }
  1868. if( i < index ) {
  1869. // Any element previous to index is given the 'past' class
  1870. element.classList.add( reverse ? 'future' : 'past' );
  1871. if( config.fragments ) {
  1872. var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
  1873. // Show all fragments on prior slides
  1874. while( pastFragments.length ) {
  1875. var pastFragment = pastFragments.pop();
  1876. pastFragment.classList.add( 'visible' );
  1877. pastFragment.classList.remove( 'current-fragment' );
  1878. }
  1879. }
  1880. }
  1881. else if( i > index ) {
  1882. // Any element subsequent to index is given the 'future' class
  1883. element.classList.add( reverse ? 'past' : 'future' );
  1884. if( config.fragments ) {
  1885. var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
  1886. // No fragments in future slides should be visible ahead of time
  1887. while( futureFragments.length ) {
  1888. var futureFragment = futureFragments.pop();
  1889. futureFragment.classList.remove( 'visible' );
  1890. futureFragment.classList.remove( 'current-fragment' );
  1891. }
  1892. }
  1893. }
  1894. }
  1895. // Mark the current slide as present
  1896. slides[index].classList.add( 'present' );
  1897. slides[index].removeAttribute( 'hidden' );
  1898. slides[index].removeAttribute( 'aria-hidden' );
  1899. // If this slide has a state associated with it, add it
  1900. // onto the current state of the deck
  1901. var slideState = slides[index].getAttribute( 'data-state' );
  1902. if( slideState ) {
  1903. state = state.concat( slideState.split( ' ' ) );
  1904. }
  1905. }
  1906. else {
  1907. // Since there are no slides we can't be anywhere beyond the
  1908. // zeroth index
  1909. index = 0;
  1910. }
  1911. return index;
  1912. }
  1913. /**
  1914. * Optimization method; hide all slides that are far away
  1915. * from the present slide.
  1916. */
  1917. function updateSlidesVisibility() {
  1918. // Select all slides and convert the NodeList result to
  1919. // an array
  1920. var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
  1921. horizontalSlidesLength = horizontalSlides.length,
  1922. distanceX,
  1923. distanceY;
  1924. if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
  1925. // The number of steps away from the present slide that will
  1926. // be visible
  1927. var viewDistance = isOverview() ? 10 : config.viewDistance;
  1928. // Limit view distance on weaker devices
  1929. if( isMobileDevice ) {
  1930. viewDistance = isOverview() ? 6 : 2;
  1931. }
  1932. // All slides need to be visible when exporting to PDF
  1933. if( isPrintingPDF() ) {
  1934. viewDistance = Number.MAX_VALUE;
  1935. }
  1936. for( var x = 0; x < horizontalSlidesLength; x++ ) {
  1937. var horizontalSlide = horizontalSlides[x];
  1938. var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
  1939. verticalSlidesLength = verticalSlides.length;
  1940. // Determine how far away this slide is from the present
  1941. distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
  1942. // If the presentation is looped, distance should measure
  1943. // 1 between the first and last slides
  1944. if( config.loop ) {
  1945. distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
  1946. }
  1947. // Show the horizontal slide if it's within the view distance
  1948. if( distanceX < viewDistance ) {
  1949. showSlide( horizontalSlide );
  1950. }
  1951. else {
  1952. hideSlide( horizontalSlide );
  1953. }
  1954. if( verticalSlidesLength ) {
  1955. var oy = getPreviousVerticalIndex( horizontalSlide );
  1956. for( var y = 0; y < verticalSlidesLength; y++ ) {
  1957. var verticalSlide = verticalSlides[y];
  1958. distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
  1959. if( distanceX + distanceY < viewDistance ) {
  1960. showSlide( verticalSlide );
  1961. }
  1962. else {
  1963. hideSlide( verticalSlide );
  1964. }
  1965. }
  1966. }
  1967. }
  1968. }
  1969. }
  1970. /**
  1971. * Pick up notes from the current slide and display tham
  1972. * to the viewer.
  1973. *
  1974. * @see `showNotes` config value
  1975. */
  1976. function updateNotes() {
  1977. if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
  1978. dom.speakerNotes.innerHTML = getSlideNotes() || '';
  1979. }
  1980. }
  1981. /**
  1982. * Updates the progress bar to reflect the current slide.
  1983. */
  1984. function updateProgress() {
  1985. // Update progress if enabled
  1986. if( config.progress && dom.progressbar ) {
  1987. dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
  1988. }
  1989. }
  1990. /**
  1991. * Updates the slide number div to reflect the current slide.
  1992. *
  1993. * The following slide number formats are available:
  1994. * "h.v": horizontal . vertical slide number (default)
  1995. * "h/v": horizontal / vertical slide number
  1996. * "c": flattened slide number
  1997. * "c/t": flattened slide number / total slides
  1998. */
  1999. function updateSlideNumber() {
  2000. // Update slide number if enabled
  2001. if( config.slideNumber && dom.slideNumber ) {
  2002. var value = [];
  2003. var format = 'h.v';
  2004. // Check if a custom number format is available
  2005. if( typeof config.slideNumber === 'string' ) {
  2006. format = config.slideNumber;
  2007. }
  2008. switch( format ) {
  2009. case 'c':
  2010. value.push( getSlidePastCount() + 1 );
  2011. break;
  2012. case 'c/t':
  2013. value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
  2014. break;
  2015. case 'h/v':
  2016. value.push( indexh + 1 );
  2017. if( isVerticalSlide() ) value.push( '/', indexv + 1 );
  2018. break;
  2019. default:
  2020. value.push( indexh + 1 );
  2021. if( isVerticalSlide() ) value.push( '.', indexv + 1 );
  2022. }
  2023. dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
  2024. }
  2025. }
  2026. /**
  2027. * Applies HTML formatting to a slide number before it's
  2028. * written to the DOM.
  2029. */
  2030. function formatSlideNumber( a, delimiter, b ) {
  2031. if( typeof b === 'number' && !isNaN( b ) ) {
  2032. return '<span class="slide-number-a">'+ a +'</span>' +
  2033. '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
  2034. '<span class="slide-number-b">'+ b +'</span>';
  2035. }
  2036. else {
  2037. return '<span class="slide-number-a">'+ a +'</span>';
  2038. }
  2039. }
  2040. /**
  2041. * Updates the state of all control/navigation arrows.
  2042. */
  2043. function updateControls() {
  2044. var routes = availableRoutes();
  2045. var fragments = availableFragments();
  2046. // Remove the 'enabled' class from all directions
  2047. dom.controlsLeft.concat( dom.controlsRight )
  2048. .concat( dom.controlsUp )
  2049. .concat( dom.controlsDown )
  2050. .concat( dom.controlsPrev )
  2051. .concat( dom.controlsNext ).forEach( function( node ) {
  2052. node.classList.remove( 'enabled' );
  2053. node.classList.remove( 'fragmented' );
  2054. } );
  2055. // Add the 'enabled' class to the available routes
  2056. if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2057. if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2058. if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2059. if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2060. // Prev/next buttons
  2061. if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2062. if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
  2063. // Highlight fragment directions
  2064. if( currentSlide ) {
  2065. // Always apply fragment decorator to prev/next buttons
  2066. if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2067. if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2068. // Apply fragment decorators to directional buttons based on
  2069. // what slide axis they are in
  2070. if( isVerticalSlide( currentSlide ) ) {
  2071. if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2072. if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2073. }
  2074. else {
  2075. if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2076. if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
  2077. }
  2078. }
  2079. }
  2080. /**
  2081. * Updates the background elements to reflect the current
  2082. * slide.
  2083. *
  2084. * @param {Boolean} includeAll If true, the backgrounds of
  2085. * all vertical slides (not just the present) will be updated.
  2086. */
  2087. function updateBackground( includeAll ) {
  2088. var currentBackground = null;
  2089. // Reverse past/future classes when in RTL mode
  2090. var horizontalPast = config.rtl ? 'future' : 'past',
  2091. horizontalFuture = config.rtl ? 'past' : 'future';
  2092. // Update the classes of all backgrounds to match the
  2093. // states of their slides (past/present/future)
  2094. toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
  2095. backgroundh.classList.remove( 'past' );
  2096. backgroundh.classList.remove( 'present' );
  2097. backgroundh.classList.remove( 'future' );
  2098. if( h < indexh ) {
  2099. backgroundh.classList.add( horizontalPast );
  2100. }
  2101. else if ( h > indexh ) {
  2102. backgroundh.classList.add( horizontalFuture );
  2103. }
  2104. else {
  2105. backgroundh.classList.add( 'present' );
  2106. // Store a reference to the current background element
  2107. currentBackground = backgroundh;
  2108. }
  2109. if( includeAll || h === indexh ) {
  2110. toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
  2111. backgroundv.classList.remove( 'past' );
  2112. backgroundv.classList.remove( 'present' );
  2113. backgroundv.classList.remove( 'future' );
  2114. if( v < indexv ) {
  2115. backgroundv.classList.add( 'past' );
  2116. }
  2117. else if ( v > indexv ) {
  2118. backgroundv.classList.add( 'future' );
  2119. }
  2120. else {
  2121. backgroundv.classList.add( 'present' );
  2122. // Only if this is the present horizontal and vertical slide
  2123. if( h === indexh ) currentBackground = backgroundv;
  2124. }
  2125. } );
  2126. }
  2127. } );
  2128. // Stop any currently playing video background
  2129. if( previousBackground ) {
  2130. var previousVideo = previousBackground.querySelector( 'video' );
  2131. if( previousVideo ) previousVideo.pause();
  2132. }
  2133. if( currentBackground ) {
  2134. // Start video playback
  2135. var currentVideo = currentBackground.querySelector( 'video' );
  2136. if( currentVideo ) {
  2137. var startVideo = function() {
  2138. currentVideo.currentTime = 0;
  2139. currentVideo.play();
  2140. currentVideo.removeEventListener( 'loadeddata', startVideo );
  2141. };
  2142. if( currentVideo.readyState > 1 ) {
  2143. startVideo();
  2144. }
  2145. else {
  2146. currentVideo.addEventListener( 'loadeddata', startVideo );
  2147. }
  2148. }
  2149. var backgroundImageURL = currentBackground.style.backgroundImage || '';
  2150. // Restart GIFs (doesn't work in Firefox)
  2151. if( /\.gif/i.test( backgroundImageURL ) ) {
  2152. currentBackground.style.backgroundImage = '';
  2153. window.getComputedStyle( currentBackground ).opacity;
  2154. currentBackground.style.backgroundImage = backgroundImageURL;
  2155. }
  2156. // Don't transition between identical backgrounds. This
  2157. // prevents unwanted flicker.
  2158. var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
  2159. var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
  2160. if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
  2161. dom.background.classList.add( 'no-transition' );
  2162. }
  2163. previousBackground = currentBackground;
  2164. }
  2165. // If there's a background brightness flag for this slide,
  2166. // bubble it to the .reveal container
  2167. if( currentSlide ) {
  2168. [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
  2169. if( currentSlide.classList.contains( classToBubble ) ) {
  2170. dom.wrapper.classList.add( classToBubble );
  2171. }
  2172. else {
  2173. dom.wrapper.classList.remove( classToBubble );
  2174. }
  2175. } );
  2176. }
  2177. // Allow the first background to apply without transition
  2178. setTimeout( function() {
  2179. dom.background.classList.remove( 'no-transition' );
  2180. }, 1 );
  2181. }
  2182. /**
  2183. * Updates the position of the parallax background based
  2184. * on the current slide index.
  2185. */
  2186. function updateParallax() {
  2187. if( config.parallaxBackgroundImage ) {
  2188. var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
  2189. verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
  2190. var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
  2191. backgroundWidth, backgroundHeight;
  2192. if( backgroundSize.length === 1 ) {
  2193. backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
  2194. }
  2195. else {
  2196. backgroundWidth = parseInt( backgroundSize[0], 10 );
  2197. backgroundHeight = parseInt( backgroundSize[1], 10 );
  2198. }
  2199. var slideWidth = dom.background.offsetWidth,
  2200. horizontalSlideCount = horizontalSlides.length,
  2201. horizontalOffsetMultiplier,
  2202. horizontalOffset;
  2203. if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
  2204. horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
  2205. }
  2206. else {
  2207. horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
  2208. }
  2209. horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
  2210. var slideHeight = dom.background.offsetHeight,
  2211. verticalSlideCount = verticalSlides.length,
  2212. verticalOffsetMultiplier,
  2213. verticalOffset;
  2214. if( typeof config.parallaxBackgroundVertical === 'number' ) {
  2215. verticalOffsetMultiplier = config.parallaxBackgroundVertical;
  2216. }
  2217. else {
  2218. verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
  2219. }
  2220. verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
  2221. dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
  2222. }
  2223. }
  2224. /**
  2225. * Called when the given slide is within the configured view
  2226. * distance. Shows the slide element and loads any content
  2227. * that is set to load lazily (data-src).
  2228. */
  2229. function showSlide( slide ) {
  2230. // Show the slide element
  2231. slide.style.display = 'block';
  2232. // Media elements with data-src attributes
  2233. toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
  2234. element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
  2235. element.removeAttribute( 'data-src' );
  2236. } );
  2237. // Media elements with <source> children
  2238. toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
  2239. var sources = 0;
  2240. toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
  2241. source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
  2242. source.removeAttribute( 'data-src' );
  2243. sources += 1;
  2244. } );
  2245. // If we rewrote sources for this video/audio element, we need
  2246. // to manually tell it to load from its new origin
  2247. if( sources > 0 ) {
  2248. media.load();
  2249. }
  2250. } );
  2251. // Show the corresponding background element
  2252. var indices = getIndices( slide );
  2253. var background = getSlideBackground( indices.h, indices.v );
  2254. if( background ) {
  2255. background.style.display = 'block';
  2256. // If the background contains media, load it
  2257. if( background.hasAttribute( 'data-loaded' ) === false ) {
  2258. background.setAttribute( 'data-loaded', 'true' );
  2259. var backgroundImage = slide.getAttribute( 'data-background-image' ),
  2260. backgroundVideo = slide.getAttribute( 'data-background-video' ),
  2261. backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
  2262. backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
  2263. backgroundIframe = slide.getAttribute( 'data-background-iframe' );
  2264. // Images
  2265. if( backgroundImage ) {
  2266. background.style.backgroundImage = 'url('+ backgroundImage +')';
  2267. }
  2268. // Videos
  2269. else if ( backgroundVideo && !isSpeakerNotes() ) {
  2270. var video = document.createElement( 'video' );
  2271. if( backgroundVideoLoop ) {
  2272. video.setAttribute( 'loop', '' );
  2273. }
  2274. if( backgroundVideoMuted ) {
  2275. video.muted = true;
  2276. }
  2277. // Support comma separated lists of video sources
  2278. backgroundVideo.split( ',' ).forEach( function( source ) {
  2279. video.innerHTML += '<source src="'+ source +'">';
  2280. } );
  2281. background.appendChild( video );
  2282. }
  2283. // Iframes
  2284. else if( backgroundIframe ) {
  2285. var iframe = document.createElement( 'iframe' );
  2286. iframe.setAttribute( 'src', backgroundIframe );
  2287. iframe.style.width = '100%';
  2288. iframe.style.height = '100%';
  2289. iframe.style.maxHeight = '100%';
  2290. iframe.style.maxWidth = '100%';
  2291. background.appendChild( iframe );
  2292. }
  2293. }
  2294. }
  2295. }
  2296. /**
  2297. * Called when the given slide is moved outside of the
  2298. * configured view distance.
  2299. */
  2300. function hideSlide( slide ) {
  2301. // Hide the slide element
  2302. slide.style.display = 'none';
  2303. // Hide the corresponding background element
  2304. var indices = getIndices( slide );
  2305. var background = getSlideBackground( indices.h, indices.v );
  2306. if( background ) {
  2307. background.style.display = 'none';
  2308. }
  2309. }
  2310. /**
  2311. * Determine what available routes there are for navigation.
  2312. *
  2313. * @return {Object} containing four booleans: left/right/up/down
  2314. */
  2315. function availableRoutes() {
  2316. var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
  2317. verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
  2318. var routes = {
  2319. left: indexh > 0 || config.loop,
  2320. right: indexh < horizontalSlides.length - 1 || config.loop,
  2321. up: indexv > 0,
  2322. down: indexv < verticalSlides.length - 1
  2323. };
  2324. // reverse horizontal controls for rtl
  2325. if( config.rtl ) {
  2326. var left = routes.left;
  2327. routes.left = routes.right;
  2328. routes.right = left;
  2329. }
  2330. return routes;
  2331. }
  2332. /**
  2333. * Returns an object describing the available fragment
  2334. * directions.
  2335. *
  2336. * @return {Object} two boolean properties: prev/next
  2337. */
  2338. function availableFragments() {
  2339. if( currentSlide && config.fragments ) {
  2340. var fragments = currentSlide.querySelectorAll( '.fragment' );
  2341. var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
  2342. return {
  2343. prev: fragments.length - hiddenFragments.length > 0,
  2344. next: !!hiddenFragments.length
  2345. };
  2346. }
  2347. else {
  2348. return { prev: false, next: false };
  2349. }
  2350. }
  2351. /**
  2352. * Enforces origin-specific format rules for embedded media.
  2353. */
  2354. function formatEmbeddedContent() {
  2355. var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
  2356. toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
  2357. var src = el.getAttribute( sourceAttribute );
  2358. if( src && src.indexOf( param ) === -1 ) {
  2359. el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
  2360. }
  2361. });
  2362. };
  2363. // YouTube frames must include "?enablejsapi=1"
  2364. _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
  2365. _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
  2366. // Vimeo frames must include "?api=1"
  2367. _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
  2368. _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
  2369. }
  2370. /**
  2371. * Start playback of any embedded content inside of
  2372. * the targeted slide.
  2373. */
  2374. function startEmbeddedContent( slide ) {
  2375. if( slide && !isSpeakerNotes() ) {
  2376. // Restart GIFs
  2377. toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
  2378. // Setting the same unchanged source like this was confirmed
  2379. // to work in Chrome, FF & Safari
  2380. el.setAttribute( 'src', el.getAttribute( 'src' ) );
  2381. } );
  2382. // HTML5 media elements
  2383. toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
  2384. if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
  2385. el.play();
  2386. }
  2387. } );
  2388. // Normal iframes
  2389. toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
  2390. startEmbeddedIframe( { target: el } );
  2391. } );
  2392. // Lazy loading iframes
  2393. toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
  2394. if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
  2395. el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
  2396. el.addEventListener( 'load', startEmbeddedIframe );
  2397. el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
  2398. }
  2399. } );
  2400. }
  2401. }
  2402. /**
  2403. * "Starts" the content of an embedded iframe using the
  2404. * postmessage API.
  2405. */
  2406. function startEmbeddedIframe( event ) {
  2407. var iframe = event.target;
  2408. // YouTube postMessage API
  2409. if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
  2410. iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
  2411. }
  2412. // Vimeo postMessage API
  2413. else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
  2414. iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
  2415. }
  2416. // Generic postMessage API
  2417. else {
  2418. iframe.contentWindow.postMessage( 'slide:start', '*' );
  2419. }
  2420. }
  2421. /**
  2422. * Stop playback of any embedded content inside of
  2423. * the targeted slide.
  2424. */
  2425. function stopEmbeddedContent( slide ) {
  2426. if( slide && slide.parentNode ) {
  2427. // HTML5 media elements
  2428. toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
  2429. if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
  2430. el.pause();
  2431. }
  2432. } );
  2433. // Generic postMessage API for non-lazy loaded iframes
  2434. toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
  2435. el.contentWindow.postMessage( 'slide:stop', '*' );
  2436. el.removeEventListener( 'load', startEmbeddedIframe );
  2437. });
  2438. // YouTube postMessage API
  2439. toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
  2440. if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
  2441. el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
  2442. }
  2443. });
  2444. // Vimeo postMessage API
  2445. toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
  2446. if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
  2447. el.contentWindow.postMessage( '{"method":"pause"}', '*' );
  2448. }
  2449. });
  2450. // Lazy loading iframes
  2451. toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
  2452. // Only removing the src doesn't actually unload the frame
  2453. // in all browsers (Firefox) so we set it to blank first
  2454. el.setAttribute( 'src', 'about:blank' );
  2455. el.removeAttribute( 'src' );
  2456. } );
  2457. }
  2458. }
  2459. /**
  2460. * Returns the number of past slides. This can be used as a global
  2461. * flattened index for slides.
  2462. */
  2463. function getSlidePastCount() {
  2464. var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
  2465. // The number of past slides
  2466. var pastCount = 0;
  2467. // Step through all slides and count the past ones
  2468. mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
  2469. var horizontalSlide = horizontalSlides[i];
  2470. var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
  2471. for( var j = 0; j < verticalSlides.length; j++ ) {
  2472. // Stop as soon as we arrive at the present
  2473. if( verticalSlides[j].classList.contains( 'present' ) ) {
  2474. break mainLoop;
  2475. }
  2476. pastCount++;
  2477. }
  2478. // Stop as soon as we arrive at the present
  2479. if( horizontalSlide.classList.contains( 'present' ) ) {
  2480. break;
  2481. }
  2482. // Don't count the wrapping section for vertical slides
  2483. if( horizontalSlide.classList.contains( 'stack' ) === false ) {
  2484. pastCount++;
  2485. }
  2486. }
  2487. return pastCount;
  2488. }
  2489. /**
  2490. * Returns a value ranging from 0-1 that represents
  2491. * how far into the presentation we have navigated.
  2492. */
  2493. function getProgress() {
  2494. // The number of past and total slides
  2495. var totalCount = getTotalSlides();
  2496. var pastCount = getSlidePastCount();
  2497. if( currentSlide ) {
  2498. var allFragments = currentSlide.querySelectorAll( '.fragment' );
  2499. // If there are fragments in the current slide those should be
  2500. // accounted for in the progress.
  2501. if( allFragments.length > 0 ) {
  2502. var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
  2503. // This value represents how big a portion of the slide progress
  2504. // that is made up by its fragments (0-1)
  2505. var fragmentWeight = 0.9;
  2506. // Add fragment progress to the past slide count
  2507. pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
  2508. }
  2509. }
  2510. return pastCount / ( totalCount - 1 );
  2511. }
  2512. /**
  2513. * Checks if this presentation is running inside of the
  2514. * speaker notes window.
  2515. */
  2516. function isSpeakerNotes() {
  2517. return !!window.location.search.match( /receiver/gi );
  2518. }
  2519. /**
  2520. * Reads the current URL (hash) and navigates accordingly.
  2521. */
  2522. function readURL() {
  2523. var hash = window.location.hash;
  2524. // Attempt to parse the hash as either an index or name
  2525. var bits = hash.slice( 2 ).split( '/' ),
  2526. name = hash.replace( /#|\//gi, '' );
  2527. // If the first bit is invalid and there is a name we can
  2528. // assume that this is a named link
  2529. if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
  2530. var element;
  2531. // Ensure the named link is a valid HTML ID attribute
  2532. if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
  2533. // Find the slide with the specified ID
  2534. element = document.getElementById( name );
  2535. }
  2536. if( element ) {
  2537. // Find the position of the named slide and navigate to it
  2538. var indices = Reveal.getIndices( element );
  2539. slide( indices.h, indices.v );
  2540. }
  2541. // If the slide doesn't exist, navigate to the current slide
  2542. else {
  2543. slide( indexh || 0, indexv || 0 );
  2544. }
  2545. }
  2546. else {
  2547. // Read the index components of the hash
  2548. var h = parseInt( bits[0], 10 ) || 0,
  2549. v = parseInt( bits[1], 10 ) || 0;
  2550. if( h !== indexh || v !== indexv ) {
  2551. slide( h, v );
  2552. }
  2553. }
  2554. }
  2555. /**
  2556. * Updates the page URL (hash) to reflect the current
  2557. * state.
  2558. *
  2559. * @param {Number} delay The time in ms to wait before
  2560. * writing the hash
  2561. */
  2562. function writeURL( delay ) {
  2563. if( config.history ) {
  2564. // Make sure there's never more than one timeout running
  2565. clearTimeout( writeURLTimeout );
  2566. // If a delay is specified, timeout this call
  2567. if( typeof delay === 'number' ) {
  2568. writeURLTimeout = setTimeout( writeURL, delay );
  2569. }
  2570. else if( currentSlide ) {
  2571. var url = '/';
  2572. // Attempt to create a named link based on the slide's ID
  2573. var id = currentSlide.getAttribute( 'id' );
  2574. if( id ) {
  2575. id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
  2576. }
  2577. // If the current slide has an ID, use that as a named link
  2578. if( typeof id === 'string' && id.length ) {
  2579. url = '/' + id;
  2580. }
  2581. // Otherwise use the /h/v index
  2582. else {
  2583. if( indexh > 0 || indexv > 0 ) url += indexh;
  2584. if( indexv > 0 ) url += '/' + indexv;
  2585. }
  2586. window.location.hash = url;
  2587. }
  2588. }
  2589. }
  2590. /**
  2591. * Retrieves the h/v location of the current, or specified,
  2592. * slide.
  2593. *
  2594. * @param {HTMLElement} slide If specified, the returned
  2595. * index will be for this slide rather than the currently
  2596. * active one
  2597. *
  2598. * @return {Object} { h: <int>, v: <int>, f: <int> }
  2599. */
  2600. function getIndices( slide ) {
  2601. // By default, return the current indices
  2602. var h = indexh,
  2603. v = indexv,
  2604. f;
  2605. // If a slide is specified, return the indices of that slide
  2606. if( slide ) {
  2607. var isVertical = isVerticalSlide( slide );
  2608. var slideh = isVertical ? slide.parentNode : slide;
  2609. // Select all horizontal slides
  2610. var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
  2611. // Now that we know which the horizontal slide is, get its index
  2612. h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
  2613. // Assume we're not vertical
  2614. v = undefined;
  2615. // If this is a vertical slide, grab the vertical index
  2616. if( isVertical ) {
  2617. v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
  2618. }
  2619. }
  2620. if( !slide && currentSlide ) {
  2621. var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
  2622. if( hasFragments ) {
  2623. var currentFragment = currentSlide.querySelector( '.current-fragment' );
  2624. if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
  2625. f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
  2626. }
  2627. else {
  2628. f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
  2629. }
  2630. }
  2631. }
  2632. return { h: h, v: v, f: f };
  2633. }
  2634. /**
  2635. * Retrieves the total number of slides in this presentation.
  2636. */
  2637. function getTotalSlides() {
  2638. return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
  2639. }
  2640. /**
  2641. * Returns the slide element matching the specified index.
  2642. */
  2643. function getSlide( x, y ) {
  2644. var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
  2645. var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
  2646. if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
  2647. return verticalSlides ? verticalSlides[ y ] : undefined;
  2648. }
  2649. return horizontalSlide;
  2650. }
  2651. /**
  2652. * Returns the background element for the given slide.
  2653. * All slides, even the ones with no background properties
  2654. * defined, have a background element so as long as the
  2655. * index is valid an element will be returned.
  2656. */
  2657. function getSlideBackground( x, y ) {
  2658. // When printing to PDF the slide backgrounds are nested
  2659. // inside of the slides
  2660. if( isPrintingPDF() ) {
  2661. var slide = getSlide( x, y );
  2662. if( slide ) {
  2663. var background = slide.querySelector( '.slide-background' );
  2664. if( background && background.parentNode === slide ) {
  2665. return background;
  2666. }
  2667. }
  2668. return undefined;
  2669. }
  2670. var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ];
  2671. var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' );
  2672. if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) {
  2673. return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined;
  2674. }
  2675. return horizontalBackground;
  2676. }
  2677. /**
  2678. * Retrieves the speaker notes from a slide. Notes can be
  2679. * defined in two ways:
  2680. * 1. As a data-notes attribute on the slide <section>
  2681. * 2. As an <aside class="notes"> inside of the slide
  2682. */
  2683. function getSlideNotes( slide ) {
  2684. // Default to the current slide
  2685. slide = slide || currentSlide;
  2686. // Notes can be specified via the data-notes attribute...
  2687. if( slide.hasAttribute( 'data-notes' ) ) {
  2688. return slide.getAttribute( 'data-notes' );
  2689. }
  2690. // ... or using an <aside class="notes"> element
  2691. var notesElement = slide.querySelector( 'aside.notes' );
  2692. if( notesElement ) {
  2693. return notesElement.innerHTML;
  2694. }
  2695. return null;
  2696. }
  2697. /**
  2698. * Retrieves the current state of the presentation as
  2699. * an object. This state can then be restored at any
  2700. * time.
  2701. */
  2702. function getState() {
  2703. var indices = getIndices();
  2704. return {
  2705. indexh: indices.h,
  2706. indexv: indices.v,
  2707. indexf: indices.f,
  2708. paused: isPaused(),
  2709. overview: isOverview()
  2710. };
  2711. }
  2712. /**
  2713. * Restores the presentation to the given state.
  2714. *
  2715. * @param {Object} state As generated by getState()
  2716. */
  2717. function setState( state ) {
  2718. if( typeof state === 'object' ) {
  2719. slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
  2720. var pausedFlag = deserialize( state.paused ),
  2721. overviewFlag = deserialize( state.overview );
  2722. if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
  2723. togglePause( pausedFlag );
  2724. }
  2725. if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
  2726. toggleOverview( overviewFlag );
  2727. }
  2728. }
  2729. }
  2730. /**
  2731. * Return a sorted fragments list, ordered by an increasing
  2732. * "data-fragment-index" attribute.
  2733. *
  2734. * Fragments will be revealed in the order that they are returned by
  2735. * this function, so you can use the index attributes to control the
  2736. * order of fragment appearance.
  2737. *
  2738. * To maintain a sensible default fragment order, fragments are presumed
  2739. * to be passed in document order. This function adds a "fragment-index"
  2740. * attribute to each node if such an attribute is not already present,
  2741. * and sets that attribute to an integer value which is the position of
  2742. * the fragment within the fragments list.
  2743. */
  2744. function sortFragments( fragments ) {
  2745. fragments = toArray( fragments );
  2746. var ordered = [],
  2747. unordered = [],
  2748. sorted = [];
  2749. // Group ordered and unordered elements
  2750. fragments.forEach( function( fragment, i ) {
  2751. if( fragment.hasAttribute( 'data-fragment-index' ) ) {
  2752. var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
  2753. if( !ordered[index] ) {
  2754. ordered[index] = [];
  2755. }
  2756. ordered[index].push( fragment );
  2757. }
  2758. else {
  2759. unordered.push( [ fragment ] );
  2760. }
  2761. } );
  2762. // Append fragments without explicit indices in their
  2763. // DOM order
  2764. ordered = ordered.concat( unordered );
  2765. // Manually count the index up per group to ensure there
  2766. // are no gaps
  2767. var index = 0;
  2768. // Push all fragments in their sorted order to an array,
  2769. // this flattens the groups
  2770. ordered.forEach( function( group ) {
  2771. group.forEach( function( fragment ) {
  2772. sorted.push( fragment );
  2773. fragment.setAttribute( 'data-fragment-index', index );
  2774. } );
  2775. index ++;
  2776. } );
  2777. return sorted;
  2778. }
  2779. /**
  2780. * Navigate to the specified slide fragment.
  2781. *
  2782. * @param {Number} index The index of the fragment that
  2783. * should be shown, -1 means all are invisible
  2784. * @param {Number} offset Integer offset to apply to the
  2785. * fragment index
  2786. *
  2787. * @return {Boolean} true if a change was made in any
  2788. * fragments visibility as part of this call
  2789. */
  2790. function navigateFragment( index, offset ) {
  2791. if( currentSlide && config.fragments ) {
  2792. var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
  2793. if( fragments.length ) {
  2794. // If no index is specified, find the current
  2795. if( typeof index !== 'number' ) {
  2796. var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
  2797. if( lastVisibleFragment ) {
  2798. index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
  2799. }
  2800. else {
  2801. index = -1;
  2802. }
  2803. }
  2804. // If an offset is specified, apply it to the index
  2805. if( typeof offset === 'number' ) {
  2806. index += offset;
  2807. }
  2808. var fragmentsShown = [],
  2809. fragmentsHidden = [];
  2810. toArray( fragments ).forEach( function( element, i ) {
  2811. if( element.hasAttribute( 'data-fragment-index' ) ) {
  2812. i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
  2813. }
  2814. // Visible fragments
  2815. if( i <= index ) {
  2816. if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
  2817. element.classList.add( 'visible' );
  2818. element.classList.remove( 'current-fragment' );
  2819. // Announce the fragments one by one to the Screen Reader
  2820. dom.statusDiv.textContent = element.textContent;
  2821. if( i === index ) {
  2822. element.classList.add( 'current-fragment' );
  2823. }
  2824. }
  2825. // Hidden fragments
  2826. else {
  2827. if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
  2828. element.classList.remove( 'visible' );
  2829. element.classList.remove( 'current-fragment' );
  2830. }
  2831. } );
  2832. if( fragmentsHidden.length ) {
  2833. dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
  2834. }
  2835. if( fragmentsShown.length ) {
  2836. dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
  2837. }
  2838. updateControls();
  2839. updateProgress();
  2840. return !!( fragmentsShown.length || fragmentsHidden.length );
  2841. }
  2842. }
  2843. return false;
  2844. }
  2845. /**
  2846. * Navigate to the next slide fragment.
  2847. *
  2848. * @return {Boolean} true if there was a next fragment,
  2849. * false otherwise
  2850. */
  2851. function nextFragment() {
  2852. return navigateFragment( null, 1 );
  2853. }
  2854. /**
  2855. * Navigate to the previous slide fragment.
  2856. *
  2857. * @return {Boolean} true if there was a previous fragment,
  2858. * false otherwise
  2859. */
  2860. function previousFragment() {
  2861. return navigateFragment( null, -1 );
  2862. }
  2863. /**
  2864. * Cues a new automated slide if enabled in the config.
  2865. */
  2866. function cueAutoSlide() {
  2867. cancelAutoSlide();
  2868. if( currentSlide ) {
  2869. var currentFragment = currentSlide.querySelector( '.current-fragment' );
  2870. var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null;
  2871. var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
  2872. var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
  2873. // Pick value in the following priority order:
  2874. // 1. Current fragment's data-autoslide
  2875. // 2. Current slide's data-autoslide
  2876. // 3. Parent slide's data-autoslide
  2877. // 4. Global autoSlide setting
  2878. if( fragmentAutoSlide ) {
  2879. autoSlide = parseInt( fragmentAutoSlide, 10 );
  2880. }
  2881. else if( slideAutoSlide ) {
  2882. autoSlide = parseInt( slideAutoSlide, 10 );
  2883. }
  2884. else if( parentAutoSlide ) {
  2885. autoSlide = parseInt( parentAutoSlide, 10 );
  2886. }
  2887. else {
  2888. autoSlide = config.autoSlide;
  2889. }
  2890. // If there are media elements with data-autoplay,
  2891. // automatically set the autoSlide duration to the
  2892. // length of that media. Not applicable if the slide
  2893. // is divided up into fragments.
  2894. if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
  2895. toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
  2896. if( el.hasAttribute( 'data-autoplay' ) ) {
  2897. if( autoSlide && el.duration * 1000 > autoSlide ) {
  2898. autoSlide = ( el.duration * 1000 ) + 1000;
  2899. }
  2900. }
  2901. } );
  2902. }
  2903. // Cue the next auto-slide if:
  2904. // - There is an autoSlide value
  2905. // - Auto-sliding isn't paused by the user
  2906. // - The presentation isn't paused
  2907. // - The overview isn't active
  2908. // - The presentation isn't over
  2909. if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
  2910. autoSlideTimeout = setTimeout( function() {
  2911. typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
  2912. cueAutoSlide();
  2913. }, autoSlide );
  2914. autoSlideStartTime = Date.now();
  2915. }
  2916. if( autoSlidePlayer ) {
  2917. autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
  2918. }
  2919. }
  2920. }
  2921. /**
  2922. * Cancels any ongoing request to auto-slide.
  2923. */
  2924. function cancelAutoSlide() {
  2925. clearTimeout( autoSlideTimeout );
  2926. autoSlideTimeout = -1;
  2927. }
  2928. function pauseAutoSlide() {
  2929. if( autoSlide && !autoSlidePaused ) {
  2930. autoSlidePaused = true;
  2931. dispatchEvent( 'autoslidepaused' );
  2932. clearTimeout( autoSlideTimeout );
  2933. if( autoSlidePlayer ) {
  2934. autoSlidePlayer.setPlaying( false );
  2935. }
  2936. }
  2937. }
  2938. function resumeAutoSlide() {
  2939. if( autoSlide && autoSlidePaused ) {
  2940. autoSlidePaused = false;
  2941. dispatchEvent( 'autoslideresumed' );
  2942. cueAutoSlide();
  2943. }
  2944. }
  2945. function navigateLeft() {
  2946. // Reverse for RTL
  2947. if( config.rtl ) {
  2948. if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
  2949. slide( indexh + 1 );
  2950. }
  2951. }
  2952. // Normal navigation
  2953. else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
  2954. slide( indexh - 1 );
  2955. }
  2956. }
  2957. function navigateRight() {
  2958. // Reverse for RTL
  2959. if( config.rtl ) {
  2960. if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
  2961. slide( indexh - 1 );
  2962. }
  2963. }
  2964. // Normal navigation
  2965. else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
  2966. slide( indexh + 1 );
  2967. }
  2968. }
  2969. function navigateUp() {
  2970. // Prioritize hiding fragments
  2971. if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
  2972. slide( indexh, indexv - 1 );
  2973. }
  2974. }
  2975. function navigateDown() {
  2976. // Prioritize revealing fragments
  2977. if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
  2978. slide( indexh, indexv + 1 );
  2979. }
  2980. }
  2981. /**
  2982. * Navigates backwards, prioritized in the following order:
  2983. * 1) Previous fragment
  2984. * 2) Previous vertical slide
  2985. * 3) Previous horizontal slide
  2986. */
  2987. function navigatePrev() {
  2988. // Prioritize revealing fragments
  2989. if( previousFragment() === false ) {
  2990. if( availableRoutes().up ) {
  2991. navigateUp();
  2992. }
  2993. else {
  2994. // Fetch the previous horizontal slide, if there is one
  2995. var previousSlide;
  2996. if( config.rtl ) {
  2997. previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
  2998. }
  2999. else {
  3000. previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
  3001. }
  3002. if( previousSlide ) {
  3003. var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
  3004. var h = indexh - 1;
  3005. slide( h, v );
  3006. }
  3007. }
  3008. }
  3009. }
  3010. /**
  3011. * The reverse of #navigatePrev().
  3012. */
  3013. function navigateNext() {
  3014. // Prioritize revealing fragments
  3015. if( nextFragment() === false ) {
  3016. if( availableRoutes().down ) {
  3017. navigateDown();
  3018. }
  3019. else if( config.rtl ) {
  3020. navigateLeft();
  3021. }
  3022. else {
  3023. navigateRight();
  3024. }
  3025. }
  3026. }
  3027. /**
  3028. * Checks if the target element prevents the triggering of
  3029. * swipe navigation.
  3030. */
  3031. function isSwipePrevented( target ) {
  3032. while( target && typeof target.hasAttribute === 'function' ) {
  3033. if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
  3034. target = target.parentNode;
  3035. }
  3036. return false;
  3037. }
  3038. // --------------------------------------------------------------------//
  3039. // ----------------------------- EVENTS -------------------------------//
  3040. // --------------------------------------------------------------------//
  3041. /**
  3042. * Called by all event handlers that are based on user
  3043. * input.
  3044. */
  3045. function onUserInput( event ) {
  3046. if( config.autoSlideStoppable ) {
  3047. pauseAutoSlide();
  3048. }
  3049. }
  3050. /**
  3051. * Handler for the document level 'keypress' event.
  3052. */
  3053. function onDocumentKeyPress( event ) {
  3054. // Check if the pressed key is question mark
  3055. if( event.shiftKey && event.charCode === 63 ) {
  3056. if( dom.overlay ) {
  3057. closeOverlay();
  3058. }
  3059. else {
  3060. showHelp( true );
  3061. }
  3062. }
  3063. }
  3064. /**
  3065. * Handler for the document level 'keydown' event.
  3066. */
  3067. function onDocumentKeyDown( event ) {
  3068. // If there's a condition specified and it returns false,
  3069. // ignore this event
  3070. if( typeof config.keyboardCondition === 'function' && config.keyboardCondition() === false ) {
  3071. return true;
  3072. }
  3073. // Remember if auto-sliding was paused so we can toggle it
  3074. var autoSlideWasPaused = autoSlidePaused;
  3075. onUserInput( event );
  3076. // Check if there's a focused element that could be using
  3077. // the keyboard
  3078. var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
  3079. var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
  3080. // Disregard the event if there's a focused element or a
  3081. // keyboard modifier key is present
  3082. if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
  3083. // While paused only allow resume keyboard events; 'b', '.''
  3084. var resumeKeyCodes = [66,190,191];
  3085. var key;
  3086. // Custom key bindings for togglePause should be able to resume
  3087. if( typeof config.keyboard === 'object' ) {
  3088. for( key in config.keyboard ) {
  3089. if( config.keyboard[key] === 'togglePause' ) {
  3090. resumeKeyCodes.push( parseInt( key, 10 ) );
  3091. }
  3092. }
  3093. }
  3094. if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
  3095. return false;
  3096. }
  3097. var triggered = false;
  3098. // 1. User defined key bindings
  3099. if( typeof config.keyboard === 'object' ) {
  3100. for( key in config.keyboard ) {
  3101. // Check if this binding matches the pressed key
  3102. if( parseInt( key, 10 ) === event.keyCode ) {
  3103. var value = config.keyboard[ key ];
  3104. // Callback function
  3105. if( typeof value === 'function' ) {
  3106. value.apply( null, [ event ] );
  3107. }
  3108. // String shortcuts to reveal.js API
  3109. else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
  3110. Reveal[ value ].call();
  3111. }
  3112. triggered = true;
  3113. }
  3114. }
  3115. }
  3116. // 2. System defined key bindings
  3117. if( triggered === false ) {
  3118. // Assume true and try to prove false
  3119. triggered = true;
  3120. switch( event.keyCode ) {
  3121. // p, page up
  3122. case 80: case 33: navigatePrev(); break;
  3123. // n, page down
  3124. case 78: case 34: navigateNext(); break;
  3125. // h, left
  3126. case 72: case 37: navigateLeft(); break;
  3127. // l, right
  3128. case 76: case 39: navigateRight(); break;
  3129. // k, up
  3130. case 75: case 38: navigateUp(); break;
  3131. // j, down
  3132. case 74: case 40: navigateDown(); break;
  3133. // home
  3134. case 36: slide( 0 ); break;
  3135. // end
  3136. case 35: slide( Number.MAX_VALUE ); break;
  3137. // space
  3138. case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
  3139. // return
  3140. case 13: isOverview() ? deactivateOverview() : triggered = false; break;
  3141. // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button
  3142. case 58: case 59: case 66: case 190: case 191: togglePause(); break;
  3143. // f
  3144. case 70: enterFullscreen(); break;
  3145. // a
  3146. case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
  3147. default:
  3148. triggered = false;
  3149. }
  3150. }
  3151. // If the input resulted in a triggered action we should prevent
  3152. // the browsers default behavior
  3153. if( triggered ) {
  3154. event.preventDefault && event.preventDefault();
  3155. }
  3156. // ESC or O key
  3157. else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
  3158. if( dom.overlay ) {
  3159. closeOverlay();
  3160. }
  3161. else {
  3162. toggleOverview();
  3163. }
  3164. event.preventDefault && event.preventDefault();
  3165. }
  3166. // If auto-sliding is enabled we need to cue up
  3167. // another timeout
  3168. cueAutoSlide();
  3169. }
  3170. /**
  3171. * Handler for the 'touchstart' event, enables support for
  3172. * swipe and pinch gestures.
  3173. */
  3174. function onTouchStart( event ) {
  3175. if( isSwipePrevented( event.target ) ) return true;
  3176. touch.startX = event.touches[0].clientX;
  3177. touch.startY = event.touches[0].clientY;
  3178. touch.startCount = event.touches.length;
  3179. // If there's two touches we need to memorize the distance
  3180. // between those two points to detect pinching
  3181. if( event.touches.length === 2 && config.overview ) {
  3182. touch.startSpan = distanceBetween( {
  3183. x: event.touches[1].clientX,
  3184. y: event.touches[1].clientY
  3185. }, {
  3186. x: touch.startX,
  3187. y: touch.startY
  3188. } );
  3189. }
  3190. }
  3191. /**
  3192. * Handler for the 'touchmove' event.
  3193. */
  3194. function onTouchMove( event ) {
  3195. if( isSwipePrevented( event.target ) ) return true;
  3196. // Each touch should only trigger one action
  3197. if( !touch.captured ) {
  3198. onUserInput( event );
  3199. var currentX = event.touches[0].clientX;
  3200. var currentY = event.touches[0].clientY;
  3201. // If the touch started with two points and still has
  3202. // two active touches; test for the pinch gesture
  3203. if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
  3204. // The current distance in pixels between the two touch points
  3205. var currentSpan = distanceBetween( {
  3206. x: event.touches[1].clientX,
  3207. y: event.touches[1].clientY
  3208. }, {
  3209. x: touch.startX,
  3210. y: touch.startY
  3211. } );
  3212. // If the span is larger than the desire amount we've got
  3213. // ourselves a pinch
  3214. if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
  3215. touch.captured = true;
  3216. if( currentSpan < touch.startSpan ) {
  3217. activateOverview();
  3218. }
  3219. else {
  3220. deactivateOverview();
  3221. }
  3222. }
  3223. event.preventDefault();
  3224. }
  3225. // There was only one touch point, look for a swipe
  3226. else if( event.touches.length === 1 && touch.startCount !== 2 ) {
  3227. var deltaX = currentX - touch.startX,
  3228. deltaY = currentY - touch.startY;
  3229. if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
  3230. touch.captured = true;
  3231. navigateLeft();
  3232. }
  3233. else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
  3234. touch.captured = true;
  3235. navigateRight();
  3236. }
  3237. else if( deltaY > touch.threshold ) {
  3238. touch.captured = true;
  3239. navigateUp();
  3240. }
  3241. else if( deltaY < -touch.threshold ) {
  3242. touch.captured = true;
  3243. navigateDown();
  3244. }
  3245. // If we're embedded, only block touch events if they have
  3246. // triggered an action
  3247. if( config.embedded ) {
  3248. if( touch.captured || isVerticalSlide( currentSlide ) ) {
  3249. event.preventDefault();
  3250. }
  3251. }
  3252. // Not embedded? Block them all to avoid needless tossing
  3253. // around of the viewport in iOS
  3254. else {
  3255. event.preventDefault();
  3256. }
  3257. }
  3258. }
  3259. // There's a bug with swiping on some Android devices unless
  3260. // the default action is always prevented
  3261. else if( UA.match( /android/gi ) ) {
  3262. event.preventDefault();
  3263. }
  3264. }
  3265. /**
  3266. * Handler for the 'touchend' event.
  3267. */
  3268. function onTouchEnd( event ) {
  3269. touch.captured = false;
  3270. }
  3271. /**
  3272. * Convert pointer down to touch start.
  3273. */
  3274. function onPointerDown( event ) {
  3275. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  3276. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  3277. onTouchStart( event );
  3278. }
  3279. }
  3280. /**
  3281. * Convert pointer move to touch move.
  3282. */
  3283. function onPointerMove( event ) {
  3284. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  3285. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  3286. onTouchMove( event );
  3287. }
  3288. }
  3289. /**
  3290. * Convert pointer up to touch end.
  3291. */
  3292. function onPointerUp( event ) {
  3293. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  3294. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  3295. onTouchEnd( event );
  3296. }
  3297. }
  3298. /**
  3299. * Handles mouse wheel scrolling, throttled to avoid skipping
  3300. * multiple slides.
  3301. */
  3302. function onDocumentMouseScroll( event ) {
  3303. if( Date.now() - lastMouseWheelStep > 600 ) {
  3304. lastMouseWheelStep = Date.now();
  3305. var delta = event.detail || -event.wheelDelta;
  3306. if( delta > 0 ) {
  3307. navigateNext();
  3308. }
  3309. else {
  3310. navigatePrev();
  3311. }
  3312. }
  3313. }
  3314. /**
  3315. * Clicking on the progress bar results in a navigation to the
  3316. * closest approximate horizontal slide using this equation:
  3317. *
  3318. * ( clickX / presentationWidth ) * numberOfSlides
  3319. */
  3320. function onProgressClicked( event ) {
  3321. onUserInput( event );
  3322. event.preventDefault();
  3323. var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
  3324. var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
  3325. if( config.rtl ) {
  3326. slideIndex = slidesTotal - slideIndex;
  3327. }
  3328. slide( slideIndex );
  3329. }
  3330. /**
  3331. * Event handler for navigation control buttons.
  3332. */
  3333. function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
  3334. function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
  3335. function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
  3336. function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
  3337. function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
  3338. function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
  3339. /**
  3340. * Handler for the window level 'hashchange' event.
  3341. */
  3342. function onWindowHashChange( event ) {
  3343. readURL();
  3344. }
  3345. /**
  3346. * Handler for the window level 'resize' event.
  3347. */
  3348. function onWindowResize( event ) {
  3349. layout();
  3350. }
  3351. /**
  3352. * Handle for the window level 'visibilitychange' event.
  3353. */
  3354. function onPageVisibilityChange( event ) {
  3355. var isHidden = document.webkitHidden ||
  3356. document.msHidden ||
  3357. document.hidden;
  3358. // If, after clicking a link or similar and we're coming back,
  3359. // focus the document.body to ensure we can use keyboard shortcuts
  3360. if( isHidden === false && document.activeElement !== document.body ) {
  3361. // Not all elements support .blur() - SVGs among them.
  3362. if( typeof document.activeElement.blur === 'function' ) {
  3363. document.activeElement.blur();
  3364. }
  3365. document.body.focus();
  3366. }
  3367. }
  3368. /**
  3369. * Invoked when a slide is and we're in the overview.
  3370. */
  3371. function onOverviewSlideClicked( event ) {
  3372. // TODO There's a bug here where the event listeners are not
  3373. // removed after deactivating the overview.
  3374. if( eventsAreBound && isOverview() ) {
  3375. event.preventDefault();
  3376. var element = event.target;
  3377. while( element && !element.nodeName.match( /section/gi ) ) {
  3378. element = element.parentNode;
  3379. }
  3380. if( element && !element.classList.contains( 'disabled' ) ) {
  3381. deactivateOverview();
  3382. if( element.nodeName.match( /section/gi ) ) {
  3383. var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
  3384. v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
  3385. slide( h, v );
  3386. }
  3387. }
  3388. }
  3389. }
  3390. /**
  3391. * Handles clicks on links that are set to preview in the
  3392. * iframe overlay.
  3393. */
  3394. function onPreviewLinkClicked( event ) {
  3395. if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
  3396. var url = event.currentTarget.getAttribute( 'href' );
  3397. if( url ) {
  3398. showPreview( url );
  3399. event.preventDefault();
  3400. }
  3401. }
  3402. }
  3403. /**
  3404. * Handles click on the auto-sliding controls element.
  3405. */
  3406. function onAutoSlidePlayerClick( event ) {
  3407. // Replay
  3408. if( Reveal.isLastSlide() && config.loop === false ) {
  3409. slide( 0, 0 );
  3410. resumeAutoSlide();
  3411. }
  3412. // Resume
  3413. else if( autoSlidePaused ) {
  3414. resumeAutoSlide();
  3415. }
  3416. // Pause
  3417. else {
  3418. pauseAutoSlide();
  3419. }
  3420. }
  3421. // --------------------------------------------------------------------//
  3422. // ------------------------ PLAYBACK COMPONENT ------------------------//
  3423. // --------------------------------------------------------------------//
  3424. /**
  3425. * Constructor for the playback component, which displays
  3426. * play/pause/progress controls.
  3427. *
  3428. * @param {HTMLElement} container The component will append
  3429. * itself to this
  3430. * @param {Function} progressCheck A method which will be
  3431. * called frequently to get the current progress on a range
  3432. * of 0-1
  3433. */
  3434. function Playback( container, progressCheck ) {
  3435. // Cosmetics
  3436. this.diameter = 100;
  3437. this.diameter2 = this.diameter/2;
  3438. this.thickness = 6;
  3439. // Flags if we are currently playing
  3440. this.playing = false;
  3441. // Current progress on a 0-1 range
  3442. this.progress = 0;
  3443. // Used to loop the animation smoothly
  3444. this.progressOffset = 1;
  3445. this.container = container;
  3446. this.progressCheck = progressCheck;
  3447. this.canvas = document.createElement( 'canvas' );
  3448. this.canvas.className = 'playback';
  3449. this.canvas.width = this.diameter;
  3450. this.canvas.height = this.diameter;
  3451. this.canvas.style.width = this.diameter2 + 'px';
  3452. this.canvas.style.height = this.diameter2 + 'px';
  3453. this.context = this.canvas.getContext( '2d' );
  3454. this.container.appendChild( this.canvas );
  3455. this.render();
  3456. }
  3457. Playback.prototype.setPlaying = function( value ) {
  3458. var wasPlaying = this.playing;
  3459. this.playing = value;
  3460. // Start repainting if we weren't already
  3461. if( !wasPlaying && this.playing ) {
  3462. this.animate();
  3463. }
  3464. else {
  3465. this.render();
  3466. }
  3467. };
  3468. Playback.prototype.animate = function() {
  3469. var progressBefore = this.progress;
  3470. this.progress = this.progressCheck();
  3471. // When we loop, offset the progress so that it eases
  3472. // smoothly rather than immediately resetting
  3473. if( progressBefore > 0.8 && this.progress < 0.2 ) {
  3474. this.progressOffset = this.progress;
  3475. }
  3476. this.render();
  3477. if( this.playing ) {
  3478. features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
  3479. }
  3480. };
  3481. /**
  3482. * Renders the current progress and playback state.
  3483. */
  3484. Playback.prototype.render = function() {
  3485. var progress = this.playing ? this.progress : 0,
  3486. radius = ( this.diameter2 ) - this.thickness,
  3487. x = this.diameter2,
  3488. y = this.diameter2,
  3489. iconSize = 28;
  3490. // Ease towards 1
  3491. this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
  3492. var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
  3493. var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
  3494. this.context.save();
  3495. this.context.clearRect( 0, 0, this.diameter, this.diameter );
  3496. // Solid background color
  3497. this.context.beginPath();
  3498. this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
  3499. this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
  3500. this.context.fill();
  3501. // Draw progress track
  3502. this.context.beginPath();
  3503. this.context.arc( x, y, radius, 0, Math.PI * 2, false );
  3504. this.context.lineWidth = this.thickness;
  3505. this.context.strokeStyle = '#666';
  3506. this.context.stroke();
  3507. if( this.playing ) {
  3508. // Draw progress on top of track
  3509. this.context.beginPath();
  3510. this.context.arc( x, y, radius, startAngle, endAngle, false );
  3511. this.context.lineWidth = this.thickness;
  3512. this.context.strokeStyle = '#fff';
  3513. this.context.stroke();
  3514. }
  3515. this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
  3516. // Draw play/pause icons
  3517. if( this.playing ) {
  3518. this.context.fillStyle = '#fff';
  3519. this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
  3520. this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
  3521. }
  3522. else {
  3523. this.context.beginPath();
  3524. this.context.translate( 4, 0 );
  3525. this.context.moveTo( 0, 0 );
  3526. this.context.lineTo( iconSize - 4, iconSize / 2 );
  3527. this.context.lineTo( 0, iconSize );
  3528. this.context.fillStyle = '#fff';
  3529. this.context.fill();
  3530. }
  3531. this.context.restore();
  3532. };
  3533. Playback.prototype.on = function( type, listener ) {
  3534. this.canvas.addEventListener( type, listener, false );
  3535. };
  3536. Playback.prototype.off = function( type, listener ) {
  3537. this.canvas.removeEventListener( type, listener, false );
  3538. };
  3539. Playback.prototype.destroy = function() {
  3540. this.playing = false;
  3541. if( this.canvas.parentNode ) {
  3542. this.container.removeChild( this.canvas );
  3543. }
  3544. };
  3545. // --------------------------------------------------------------------//
  3546. // ------------------------------- API --------------------------------//
  3547. // --------------------------------------------------------------------//
  3548. Reveal = {
  3549. VERSION: VERSION,
  3550. initialize: initialize,
  3551. configure: configure,
  3552. sync: sync,
  3553. // Navigation methods
  3554. slide: slide,
  3555. left: navigateLeft,
  3556. right: navigateRight,
  3557. up: navigateUp,
  3558. down: navigateDown,
  3559. prev: navigatePrev,
  3560. next: navigateNext,
  3561. // Fragment methods
  3562. navigateFragment: navigateFragment,
  3563. prevFragment: previousFragment,
  3564. nextFragment: nextFragment,
  3565. // Deprecated aliases
  3566. navigateTo: slide,
  3567. navigateLeft: navigateLeft,
  3568. navigateRight: navigateRight,
  3569. navigateUp: navigateUp,
  3570. navigateDown: navigateDown,
  3571. navigatePrev: navigatePrev,
  3572. navigateNext: navigateNext,
  3573. // Forces an update in slide layout
  3574. layout: layout,
  3575. // Randomizes the order of slides
  3576. shuffle: shuffle,
  3577. // Returns an object with the available routes as booleans (left/right/top/bottom)
  3578. availableRoutes: availableRoutes,
  3579. // Returns an object with the available fragments as booleans (prev/next)
  3580. availableFragments: availableFragments,
  3581. // Toggles the overview mode on/off
  3582. toggleOverview: toggleOverview,
  3583. // Toggles the "black screen" mode on/off
  3584. togglePause: togglePause,
  3585. // Toggles the auto slide mode on/off
  3586. toggleAutoSlide: toggleAutoSlide,
  3587. // State checks
  3588. isOverview: isOverview,
  3589. isPaused: isPaused,
  3590. isAutoSliding: isAutoSliding,
  3591. // Adds or removes all internal event listeners (such as keyboard)
  3592. addEventListeners: addEventListeners,
  3593. removeEventListeners: removeEventListeners,
  3594. // Facility for persisting and restoring the presentation state
  3595. getState: getState,
  3596. setState: setState,
  3597. // Presentation progress on range of 0-1
  3598. getProgress: getProgress,
  3599. // Returns the indices of the current, or specified, slide
  3600. getIndices: getIndices,
  3601. getTotalSlides: getTotalSlides,
  3602. // Returns the slide element at the specified index
  3603. getSlide: getSlide,
  3604. // Returns the slide background element at the specified index
  3605. getSlideBackground: getSlideBackground,
  3606. // Returns the speaker notes string for a slide, or null
  3607. getSlideNotes: getSlideNotes,
  3608. // Returns the previous slide element, may be null
  3609. getPreviousSlide: function() {
  3610. return previousSlide;
  3611. },
  3612. // Returns the current slide element
  3613. getCurrentSlide: function() {
  3614. return currentSlide;
  3615. },
  3616. // Returns the current scale of the presentation content
  3617. getScale: function() {
  3618. return scale;
  3619. },
  3620. // Returns the current configuration object
  3621. getConfig: function() {
  3622. return config;
  3623. },
  3624. // Helper method, retrieves query string as a key/value hash
  3625. getQueryHash: function() {
  3626. var query = {};
  3627. location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
  3628. query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
  3629. } );
  3630. // Basic deserialization
  3631. for( var i in query ) {
  3632. var value = query[ i ];
  3633. query[ i ] = deserialize( unescape( value ) );
  3634. }
  3635. return query;
  3636. },
  3637. // Returns true if we're currently on the first slide
  3638. isFirstSlide: function() {
  3639. return ( indexh === 0 && indexv === 0 );
  3640. },
  3641. // Returns true if we're currently on the last slide
  3642. isLastSlide: function() {
  3643. if( currentSlide ) {
  3644. // Does this slide has next a sibling?
  3645. if( currentSlide.nextElementSibling ) return false;
  3646. // If it's vertical, does its parent have a next sibling?
  3647. if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
  3648. return true;
  3649. }
  3650. return false;
  3651. },
  3652. // Checks if reveal.js has been loaded and is ready for use
  3653. isReady: function() {
  3654. return loaded;
  3655. },
  3656. // Forward event binding to the reveal DOM element
  3657. addEventListener: function( type, listener, useCapture ) {
  3658. if( 'addEventListener' in window ) {
  3659. ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
  3660. }
  3661. },
  3662. removeEventListener: function( type, listener, useCapture ) {
  3663. if( 'addEventListener' in window ) {
  3664. ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
  3665. }
  3666. },
  3667. // Programatically triggers a keyboard event
  3668. triggerKey: function( keyCode ) {
  3669. onDocumentKeyDown( { keyCode: keyCode } );
  3670. },
  3671. // Registers a new shortcut to include in the help overlay
  3672. registerKeyboardShortcut: function( key, value ) {
  3673. keyboardShortcuts[key] = value;
  3674. }
  3675. };
  3676. return Reveal;
  3677. }));