overlay.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * Handles the display of reveal.js' overlay elements used
  3. * to preview iframes, images & videos.
  4. */
  5. export default class Overlay {
  6. constructor( Reveal ) {
  7. this.Reveal = Reveal;
  8. this.onSlidesClicked = this.onSlidesClicked.bind( this );
  9. this.iframeTriggerSelector = null;
  10. this.mediaTriggerSelector = '[data-preview-image], [data-preview-video]';
  11. this.stateProps = ['previewIframe', 'previewImage', 'previewVideo', 'previewFit'];
  12. this.state = {};
  13. }
  14. update() {
  15. // Enable link previews globally
  16. if( this.Reveal.getConfig().previewLinks ) {
  17. this.iframeTriggerSelector = 'a[href]:not([data-preview-link=false]), [data-preview-link]:not(a):not([data-preview-link=false])';
  18. }
  19. // Enable link previews for individual elements
  20. else {
  21. this.iframeTriggerSelector = '[data-preview-link]:not([data-preview-link=false])';
  22. }
  23. const hasLinkPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.iframeTriggerSelector ).length > 0;
  24. const hasMediaPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.mediaTriggerSelector ).length > 0;
  25. // Only add the listener when there are previewable elements in the slides
  26. if( hasLinkPreviews || hasMediaPreviews ) {
  27. this.Reveal.getSlidesElement().addEventListener( 'click', this.onSlidesClicked, false );
  28. }
  29. else {
  30. this.Reveal.getSlidesElement().removeEventListener( 'click', this.onSlidesClicked, false );
  31. }
  32. }
  33. createOverlay( className ) {
  34. this.dom = document.createElement( 'div' );
  35. this.dom.classList.add( 'r-overlay' );
  36. this.dom.classList.add( className );
  37. this.viewport = document.createElement( 'div' );
  38. this.viewport.classList.add( 'r-overlay-viewport' );
  39. this.dom.appendChild( this.viewport );
  40. this.Reveal.getRevealElement().appendChild( this.dom );
  41. }
  42. /**
  43. * Opens a lightbox that previews the target URL.
  44. *
  45. * @param {string} url - url for lightbox iframe src
  46. */
  47. previewIframe( url ) {
  48. this.close();
  49. this.state = { previewIframe: url };
  50. this.createOverlay( 'r-overlay-preview' );
  51. this.dom.dataset.state = 'loading';
  52. this.viewport.innerHTML =
  53. `<header class="r-overlay-header">
  54. <a class="r-overlay-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
  55. <button class="r-overlay-button r-overlay-close"><span class="icon"></span></button>
  56. </header>
  57. <div class="r-overlay-spinner"></div>
  58. <div class="r-overlay-content">
  59. <iframe src="${url}"></iframe>
  60. <small class="r-overlay-content-inner">
  61. <span class="r-overlay-error x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
  62. </small>
  63. </div>`;
  64. this.dom.querySelector( 'iframe' ).addEventListener( 'load', event => {
  65. this.dom.dataset.state = 'loaded';
  66. }, false );
  67. this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
  68. this.close();
  69. event.preventDefault();
  70. }, false );
  71. this.dom.querySelector( '.r-overlay-external' ).addEventListener( 'click', event => {
  72. this.close();
  73. }, false );
  74. this.Reveal.dispatchEvent({ type: 'previewiframe', data: { url } });
  75. }
  76. /**
  77. * Opens a lightbox window that provides a larger view of the
  78. * given image/video.
  79. *
  80. * @param {string} url - url to the image/video to preview
  81. * @param {image|video} mediaType
  82. * @param {string} [fitMode] - the fit mode to use for the preview
  83. */
  84. previewMedia( url, mediaType, fitMode ) {
  85. if( mediaType !== 'image' && mediaType !== 'video' ) {
  86. console.warn( 'Please specify a valid media type to preview (image|video)' );
  87. return;
  88. }
  89. this.close();
  90. fitMode = fitMode || 'scale-down';
  91. this.createOverlay( 'r-overlay-preview' );
  92. this.dom.dataset.state = 'loading';
  93. this.dom.dataset.previewFit = fitMode;
  94. this.viewport.innerHTML =
  95. `<header class="r-overlay-header">
  96. <button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
  97. </header>
  98. <div class="r-overlay-spinner"></div>
  99. <div class="r-overlay-content"></div>`;
  100. const contentElement = this.dom.querySelector( '.r-overlay-content' );
  101. if( mediaType === 'image' ) {
  102. this.state = { previewImage: url, previewFit: fitMode }
  103. const img = document.createElement( 'img', {} );
  104. img.src = url;
  105. contentElement.appendChild( img );
  106. img.addEventListener( 'load', () => {
  107. this.dom.dataset.state = 'loaded';
  108. }, false );
  109. img.addEventListener( 'error', () => {
  110. this.dom.dataset.state = 'error';
  111. contentElement.innerHTML =
  112. `<span class="r-overlay-error">Unable to load image.</span>`
  113. }, false );
  114. // Hide image overlays when clicking outside the overlay
  115. this.dom.style.cursor = 'zoom-out';
  116. this.dom.addEventListener( 'click', ( event ) => {
  117. this.close();
  118. }, false );
  119. this.Reveal.dispatchEvent({ type: 'previewimage', data: { url } });
  120. }
  121. else if( mediaType === 'video' ) {
  122. this.state = { previewVideo: url, previewFit: fitMode }
  123. const video = document.createElement( 'video' );
  124. video.autoplay = this.dom.dataset.previewAutoplay === 'false' ? false : true;
  125. video.controls = this.dom.dataset.previewControls === 'false' ? false : true;
  126. video.loop = this.dom.dataset.previewLoop === 'true' ? true : false;
  127. video.muted = this.dom.dataset.previewMuted === 'true' ? true : false;
  128. video.playsInline = true;
  129. video.src = url;
  130. contentElement.appendChild( video );
  131. video.addEventListener( 'loadeddata', () => {
  132. this.dom.dataset.state = 'loaded';
  133. }, false );
  134. video.addEventListener( 'error', () => {
  135. this.dom.dataset.state = 'error';
  136. contentElement.innerHTML =
  137. `<span class="r-overlay-error">Unable to load video.</span>`;
  138. }, false );
  139. this.Reveal.dispatchEvent({ type: 'previewvideo', data: { url } });
  140. }
  141. else {
  142. throw new Error( 'Please specify a valid media type to preview' );
  143. }
  144. this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', ( event ) => {
  145. this.close();
  146. event.preventDefault();
  147. }, false );
  148. }
  149. previewImage( url, fitMode ) {
  150. this.previewMedia( url, 'image', fitMode );
  151. }
  152. previewVideo( url, fitMode ) {
  153. this.previewMedia( url, 'video', fitMode );
  154. }
  155. /**
  156. * Open or close help overlay window.
  157. *
  158. * @param {Boolean} [override] Flag which overrides the
  159. * toggle logic and forcibly sets the desired state. True means
  160. * help is open, false means it's closed.
  161. */
  162. toggleHelp( override ) {
  163. if( typeof override === 'boolean' ) {
  164. override ? this.showHelp() : this.close();
  165. }
  166. else {
  167. if( this.dom ) {
  168. this.close();
  169. }
  170. else {
  171. this.showHelp();
  172. }
  173. }
  174. }
  175. /**
  176. * Opens an overlay window with help material.
  177. */
  178. showHelp() {
  179. if( this.Reveal.getConfig().help ) {
  180. this.close();
  181. this.createOverlay( 'r-overlay-help' );
  182. let html = '<p class="title">Keyboard Shortcuts</p>';
  183. let shortcuts = this.Reveal.keyboard.getShortcuts(),
  184. bindings = this.Reveal.keyboard.getBindings();
  185. html += '<table><th>KEY</th><th>ACTION</th>';
  186. for( let key in shortcuts ) {
  187. html += `<tr><td>${key}</td><td>${shortcuts[ key ]}</td></tr>`;
  188. }
  189. // Add custom key bindings that have associated descriptions
  190. for( let binding in bindings ) {
  191. if( bindings[binding].key && bindings[binding].description ) {
  192. html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
  193. }
  194. }
  195. html += '</table>';
  196. this.viewport.innerHTML = `
  197. <header class="r-overlay-header">
  198. <button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
  199. </header>
  200. <div class="r-overlay-content">
  201. <div class="r-overlay-help-content">${html}</div>
  202. </div>
  203. `;
  204. this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
  205. this.close();
  206. event.preventDefault();
  207. }, false );
  208. this.Reveal.dispatchEvent({ type: 'showhelp' });
  209. }
  210. }
  211. isOpen() {
  212. return !!this.dom;
  213. }
  214. /**
  215. * Closes any currently open overlay.
  216. */
  217. close() {
  218. if( this.dom ) {
  219. this.dom.remove();
  220. this.dom = null;
  221. this.state = {};
  222. this.Reveal.dispatchEvent({ type: 'closeoverlay' });
  223. return true;
  224. }
  225. return false;
  226. }
  227. getState() {
  228. return this.state;
  229. }
  230. setState( state ) {
  231. // Ignore the incoming state if none of the preview related
  232. // props have changed
  233. if( this.stateProps.every( key => this.state[ key ] === state[ key ] ) ) {
  234. return;
  235. }
  236. if( state.previewIframe ) {
  237. this.previewIframe( state.previewIframe );
  238. }
  239. else if( state.previewImage ) {
  240. this.previewImage( state.previewImage, state.previewFit );
  241. }
  242. else if( state.previewVideo ) {
  243. this.previewVideo( state.previewVideo, state.previewFit );
  244. }
  245. else {
  246. this.close();
  247. }
  248. }
  249. onSlidesClicked( event ) {
  250. const target = event.target;
  251. const linkTarget = target.closest( this.iframeTriggerSelector );
  252. const mediaTarget = target.closest( this.mediaTriggerSelector );
  253. // Was an iframe lightbox trigger clicked?
  254. if( linkTarget ) {
  255. if( event.metaKey || event.shiftKey || event.altKey ) {
  256. // Let the browser handle meta keys naturally so users can cmd+click
  257. return;
  258. }
  259. let url = linkTarget.getAttribute( 'href' ) || linkTarget.getAttribute( 'data-preview-link' );
  260. if( url ) {
  261. this.previewIframe( url );
  262. event.preventDefault();
  263. }
  264. }
  265. // Was a media lightbox trigger clicked?
  266. else if( mediaTarget ) {
  267. if( mediaTarget.hasAttribute( 'data-preview-image' ) ) {
  268. let url = mediaTarget.dataset.previewImage || mediaTarget.getAttribute( 'src' );
  269. if( url ) {
  270. this.previewImage( url, mediaTarget.dataset.previewFit );
  271. event.preventDefault();
  272. }
  273. }
  274. else if( mediaTarget.hasAttribute( 'data-preview-video' ) ) {
  275. let url = mediaTarget.dataset.previewVideo || mediaTarget.getAttribute( 'src' );
  276. if( !url ) {
  277. let source = mediaTarget.querySelector( 'source' );
  278. if( source ) {
  279. url = source.getAttribute( 'src' );
  280. }
  281. }
  282. if( url ) {
  283. this.previewVideo( url, mediaTarget.dataset.previewFit );
  284. event.preventDefault();
  285. }
  286. }
  287. }
  288. }
  289. destroy() {
  290. this.close();
  291. }
  292. }