overlay.js 8.4 KB

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