slidecontent.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import { HORIZONTAL_SLIDES_SELECTOR, VERTICAL_SLIDES_SELECTOR } from '../utils/constants.js'
  2. import { extend, toArray, closestParent } from '../utils/util.js'
  3. /**
  4. * Handles loading, unloading and playback of slide
  5. * content such as images, videos and iframes.
  6. */
  7. export default class SlideContent {
  8. constructor( Reveal ) {
  9. this.Reveal = Reveal;
  10. }
  11. /**
  12. * Called when the given slide is within the configured view
  13. * distance. Shows the slide element and loads any content
  14. * that is set to load lazily (data-src).
  15. *
  16. * @param {HTMLElement} slide Slide to show
  17. */
  18. load( slide, options = {} ) {
  19. // Show the slide element
  20. slide.style.display = this.Reveal.getConfig().display;
  21. // Media elements with data-src attributes
  22. toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( element => {
  23. if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
  24. element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
  25. element.setAttribute( 'data-lazy-loaded', '' );
  26. element.removeAttribute( 'data-src' );
  27. }
  28. } );
  29. // Media elements with <source> children
  30. toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( media => {
  31. let sources = 0;
  32. toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( source => {
  33. source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
  34. source.removeAttribute( 'data-src' );
  35. source.setAttribute( 'data-lazy-loaded', '' );
  36. sources += 1;
  37. } );
  38. // If we rewrote sources for this video/audio element, we need
  39. // to manually tell it to load from its new origin
  40. if( sources > 0 ) {
  41. media.load();
  42. }
  43. } );
  44. // Show the corresponding background element
  45. let background = slide.slideBackgroundElement;
  46. if( background ) {
  47. background.style.display = 'block';
  48. let backgroundContent = slide.slideBackgroundContentElement;
  49. let backgroundIframe = slide.getAttribute( 'data-background-iframe' );
  50. // If the background contains media, load it
  51. if( background.hasAttribute( 'data-loaded' ) === false ) {
  52. background.setAttribute( 'data-loaded', 'true' );
  53. let backgroundImage = slide.getAttribute( 'data-background-image' ),
  54. backgroundVideo = slide.getAttribute( 'data-background-video' ),
  55. backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
  56. backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );
  57. // Images
  58. if( backgroundImage ) {
  59. backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')';
  60. }
  61. // Videos
  62. else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
  63. let video = document.createElement( 'video' );
  64. if( backgroundVideoLoop ) {
  65. video.setAttribute( 'loop', '' );
  66. }
  67. if( backgroundVideoMuted ) {
  68. video.muted = true;
  69. }
  70. // Inline video playback works (at least in Mobile Safari) as
  71. // long as the video is muted and the `playsinline` attribute is
  72. // present
  73. if( isMobileDevice ) {
  74. video.muted = true;
  75. video.autoplay = true;
  76. video.setAttribute( 'playsinline', '' );
  77. }
  78. // Support comma separated lists of video sources
  79. backgroundVideo.split( ',' ).forEach( source => {
  80. video.innerHTML += '<source src="'+ source +'">';
  81. } );
  82. backgroundContent.appendChild( video );
  83. }
  84. // Iframes
  85. else if( backgroundIframe && options.excludeIframes !== true ) {
  86. let iframe = document.createElement( 'iframe' );
  87. iframe.setAttribute( 'allowfullscreen', '' );
  88. iframe.setAttribute( 'mozallowfullscreen', '' );
  89. iframe.setAttribute( 'webkitallowfullscreen', '' );
  90. iframe.setAttribute( 'allow', 'autoplay' );
  91. iframe.setAttribute( 'data-src', backgroundIframe );
  92. iframe.style.width = '100%';
  93. iframe.style.height = '100%';
  94. iframe.style.maxHeight = '100%';
  95. iframe.style.maxWidth = '100%';
  96. backgroundContent.appendChild( iframe );
  97. }
  98. }
  99. // Start loading preloadable iframes
  100. let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );
  101. if( backgroundIframeElement ) {
  102. // Check if this iframe is eligible to be preloaded
  103. if( shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
  104. if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
  105. backgroundIframeElement.setAttribute( 'src', backgroundIframe );
  106. }
  107. }
  108. }
  109. }
  110. }
  111. /**
  112. * Unloads and hides the given slide. This is called when the
  113. * slide is moved outside of the configured view distance.
  114. *
  115. * @param {HTMLElement} slide
  116. */
  117. unload( slide ) {
  118. // Hide the slide element
  119. slide.style.display = 'none';
  120. // Hide the corresponding background element
  121. let background = this.Reveal.getSlideBackground( slide );
  122. if( background ) {
  123. background.style.display = 'none';
  124. // Unload any background iframes
  125. toArray( background.querySelectorAll( 'iframe[src]' ) ).forEach( element => {
  126. element.removeAttribute( 'src' );
  127. } );
  128. }
  129. // Reset lazy-loaded media elements with src attributes
  130. toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( element => {
  131. element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
  132. element.removeAttribute( 'src' );
  133. } );
  134. // Reset lazy-loaded media elements with <source> children
  135. toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( source => {
  136. source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
  137. source.removeAttribute( 'src' );
  138. } );
  139. }
  140. /**
  141. * Enforces origin-specific format rules for embedded media.
  142. */
  143. formatEmbeddedContent() {
  144. let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {
  145. toArray( this.Reveal.getSlidesElement().querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( el => {
  146. let src = el.getAttribute( sourceAttribute );
  147. if( src && src.indexOf( param ) === -1 ) {
  148. el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
  149. }
  150. });
  151. };
  152. // YouTube frames must include "?enablejsapi=1"
  153. _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
  154. _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
  155. // Vimeo frames must include "?api=1"
  156. _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
  157. _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
  158. }
  159. /**
  160. * Start playback of any embedded content inside of
  161. * the given element.
  162. *
  163. * @param {HTMLElement} element
  164. */
  165. startEmbeddedContent( element ) {
  166. if( element && !this.Reveal.isSpeakerNotes() ) {
  167. // Restart GIFs
  168. toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( el => {
  169. // Setting the same unchanged source like this was confirmed
  170. // to work in Chrome, FF & Safari
  171. el.setAttribute( 'src', el.getAttribute( 'src' ) );
  172. } );
  173. // HTML5 media elements
  174. toArray( element.querySelectorAll( 'video, audio' ) ).forEach( el => {
  175. if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
  176. return;
  177. }
  178. // Prefer an explicit global autoplay setting
  179. let autoplay = config.autoPlayMedia;
  180. // If no global setting is available, fall back on the element's
  181. // own autoplay setting
  182. if( typeof autoplay !== 'boolean' ) {
  183. autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
  184. }
  185. if( autoplay && typeof el.play === 'function' ) {
  186. // If the media is ready, start playback
  187. if( el.readyState > 1 ) {
  188. this.startEmbeddedMedia( { target: el } );
  189. }
  190. // Mobile devices never fire a loaded event so instead
  191. // of waiting, we initiate playback
  192. else if( isMobileDevice ) {
  193. let promise = el.play();
  194. // If autoplay does not work, ensure that the controls are visible so
  195. // that the viewer can start the media on their own
  196. if( promise && typeof promise.catch === 'function' && el.controls === false ) {
  197. promise.catch( () => {
  198. el.controls = true;
  199. // Once the video does start playing, hide the controls again
  200. el.addEventListener( 'play', () => {
  201. el.controls = false;
  202. } );
  203. } );
  204. }
  205. }
  206. // If the media isn't loaded, wait before playing
  207. else {
  208. el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes
  209. el.addEventListener( 'loadeddata', this.startEmbeddedMedia );
  210. }
  211. }
  212. } );
  213. // Normal iframes
  214. toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( el => {
  215. if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
  216. return;
  217. }
  218. this.startEmbeddedIframe( { target: el } );
  219. } );
  220. // Lazy loading iframes
  221. toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( el => {
  222. if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
  223. return;
  224. }
  225. if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
  226. el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
  227. el.addEventListener( 'load', this.startEmbeddedIframe );
  228. el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
  229. }
  230. } );
  231. }
  232. }
  233. /**
  234. * Starts playing an embedded video/audio element after
  235. * it has finished loading.
  236. *
  237. * @param {object} event
  238. */
  239. startEmbeddedMedia( event ) {
  240. let isAttachedToDOM = !!closestParent( event.target, 'html' ),
  241. isVisible = !!closestParent( event.target, '.present' );
  242. if( isAttachedToDOM && isVisible ) {
  243. event.target.currentTime = 0;
  244. event.target.play();
  245. }
  246. event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
  247. }
  248. /**
  249. * "Starts" the content of an embedded iframe using the
  250. * postMessage API.
  251. *
  252. * @param {object} event
  253. */
  254. startEmbeddedIframe( event ) {
  255. let iframe = event.target;
  256. if( iframe && iframe.contentWindow ) {
  257. let isAttachedToDOM = !!closestParent( event.target, 'html' ),
  258. isVisible = !!closestParent( event.target, '.present' );
  259. if( isAttachedToDOM && isVisible ) {
  260. // Prefer an explicit global autoplay setting
  261. let autoplay = config.autoPlayMedia;
  262. // If no global setting is available, fall back on the element's
  263. // own autoplay setting
  264. if( typeof autoplay !== 'boolean' ) {
  265. autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
  266. }
  267. // YouTube postMessage API
  268. if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
  269. iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
  270. }
  271. // Vimeo postMessage API
  272. else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
  273. iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
  274. }
  275. // Generic postMessage API
  276. else {
  277. iframe.contentWindow.postMessage( 'slide:start', '*' );
  278. }
  279. }
  280. }
  281. }
  282. /**
  283. * Stop playback of any embedded content inside of
  284. * the targeted slide.
  285. *
  286. * @param {HTMLElement} element
  287. */
  288. stopEmbeddedContent( element, options = {} ) {
  289. options = extend( {
  290. // Defaults
  291. unloadIframes: true
  292. }, options );
  293. if( element && element.parentNode ) {
  294. // HTML5 media elements
  295. toArray( element.querySelectorAll( 'video, audio' ) ).forEach( el => {
  296. if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
  297. el.setAttribute('data-paused-by-reveal', '');
  298. el.pause();
  299. }
  300. } );
  301. // Generic postMessage API for non-lazy loaded iframes
  302. toArray( element.querySelectorAll( 'iframe' ) ).forEach( el => {
  303. if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
  304. el.removeEventListener( 'load', this.startEmbeddedIframe );
  305. });
  306. // YouTube postMessage API
  307. toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( el => {
  308. if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
  309. el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
  310. }
  311. });
  312. // Vimeo postMessage API
  313. toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( el => {
  314. if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
  315. el.contentWindow.postMessage( '{"method":"pause"}', '*' );
  316. }
  317. });
  318. if( options.unloadIframes === true ) {
  319. // Unload lazy-loaded iframes
  320. toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( el => {
  321. // Only removing the src doesn't actually unload the frame
  322. // in all browsers (Firefox) so we set it to blank first
  323. el.setAttribute( 'src', 'about:blank' );
  324. el.removeAttribute( 'src' );
  325. } );
  326. }
  327. }
  328. }
  329. }