1
0

slidecontent.js 13 KB

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