overlay.js 9.5 KB

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