You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

435 lines
13KB

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