plugin.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import speakerViewHTML from './speaker-view.html'
  2. import { marked } from 'marked';
  3. /**
  4. * Handles opening of and synchronization with the reveal.js
  5. * notes window.
  6. *
  7. * Handshake process:
  8. * 1. This window posts 'connect' to notes window
  9. * - Includes URL of presentation to show
  10. * 2. Notes window responds with 'connected' when it is available
  11. * 3. This window proceeds to send the current presentation state
  12. * to the notes window
  13. */
  14. const Plugin = () => {
  15. let connectInterval;
  16. let speakerWindow = null;
  17. let deck;
  18. /**
  19. * Opens a new speaker view window.
  20. */
  21. function openSpeakerWindow() {
  22. // If a window is already open, focus it
  23. if( speakerWindow && !speakerWindow.closed ) {
  24. speakerWindow.focus();
  25. }
  26. else {
  27. speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
  28. speakerWindow.marked = marked;
  29. speakerWindow.document.write( speakerViewHTML );
  30. if( !speakerWindow ) {
  31. alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
  32. return;
  33. }
  34. connect();
  35. }
  36. }
  37. /**
  38. * Reconnect with an existing speaker view window.
  39. */
  40. function reconnectSpeakerWindow( reconnectWindow ) {
  41. if( speakerWindow && !speakerWindow.closed ) {
  42. speakerWindow.focus();
  43. }
  44. else {
  45. speakerWindow = reconnectWindow;
  46. window.addEventListener( 'message', onPostMessage );
  47. onConnected();
  48. }
  49. }
  50. /**
  51. * Connect to the notes window through a postmessage handshake.
  52. * Using postmessage enables us to work in situations where the
  53. * origins differ, such as a presentation being opened from the
  54. * file system.
  55. */
  56. function connect() {
  57. const presentationURL = deck.getConfig().url;
  58. const url = typeof presentationURL === 'string' ? presentationURL :
  59. window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;
  60. // Keep trying to connect until we get a 'connected' message back
  61. connectInterval = setInterval( function() {
  62. speakerWindow.postMessage( JSON.stringify( {
  63. namespace: 'reveal-notes',
  64. type: 'connect',
  65. state: deck.getState(),
  66. url
  67. } ), '*' );
  68. }, 500 );
  69. window.addEventListener( 'message', onPostMessage );
  70. }
  71. /**
  72. * Calls the specified Reveal.js method with the provided argument
  73. * and then pushes the result to the notes frame.
  74. */
  75. function callRevealApi( methodName, methodArguments, callId ) {
  76. let result = deck[methodName].apply( deck, methodArguments );
  77. speakerWindow.postMessage( JSON.stringify( {
  78. namespace: 'reveal-notes',
  79. type: 'return',
  80. result,
  81. callId
  82. } ), '*' );
  83. }
  84. /**
  85. * Posts the current slide data to the notes window.
  86. */
  87. function post( event ) {
  88. let slideElement = deck.getCurrentSlide(),
  89. notesElements = slideElement.querySelectorAll( 'aside.notes' ),
  90. fragmentElement = slideElement.querySelector( '.current-fragment' );
  91. let messageData = {
  92. namespace: 'reveal-notes',
  93. type: 'state',
  94. notes: '',
  95. markdown: false,
  96. whitespace: 'normal',
  97. state: deck.getState()
  98. };
  99. // Look for notes defined in a slide attribute
  100. if( slideElement.hasAttribute( 'data-notes' ) ) {
  101. messageData.notes = slideElement.getAttribute( 'data-notes' );
  102. messageData.whitespace = 'pre-wrap';
  103. }
  104. // Look for notes defined in a fragment
  105. if( fragmentElement ) {
  106. let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
  107. if( fragmentNotes ) {
  108. messageData.notes = fragmentNotes.innerHTML;
  109. messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';
  110. // Ignore other slide notes
  111. notesElements = null;
  112. }
  113. else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
  114. messageData.notes = fragmentElement.getAttribute( 'data-notes' );
  115. messageData.whitespace = 'pre-wrap';
  116. // In case there are slide notes
  117. notesElements = null;
  118. }
  119. }
  120. // Look for notes defined in an aside element
  121. if( notesElements && notesElements.length ) {
  122. // Ignore notes inside of fragments since those are shown
  123. // individually when stepping through fragments
  124. notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );
  125. messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );
  126. messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
  127. }
  128. speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
  129. }
  130. /**
  131. * Check if the given event is from the same origin as the
  132. * current window.
  133. */
  134. function isSameOriginEvent( event ) {
  135. try {
  136. return window.location.origin === event.source.location.origin;
  137. }
  138. catch ( error ) {
  139. return false;
  140. }
  141. }
  142. function onPostMessage( event ) {
  143. // Only allow same-origin messages
  144. // (added 12/5/22 as a XSS safeguard)
  145. if( isSameOriginEvent( event ) ) {
  146. try {
  147. let data = JSON.parse( event.data );
  148. if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
  149. clearInterval( connectInterval );
  150. onConnected();
  151. }
  152. else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
  153. callRevealApi( data.methodName, data.arguments, data.callId );
  154. }
  155. } catch (e) {}
  156. }
  157. }
  158. /**
  159. * Called once we have established a connection to the notes
  160. * window.
  161. */
  162. function onConnected() {
  163. // Monitor events that trigger a change in state
  164. deck.on( 'slidechanged', post );
  165. deck.on( 'fragmentshown', post );
  166. deck.on( 'fragmenthidden', post );
  167. deck.on( 'overviewhidden', post );
  168. deck.on( 'overviewshown', post );
  169. deck.on( 'paused', post );
  170. deck.on( 'resumed', post );
  171. deck.on( 'showmediapreview', post );
  172. deck.on( 'showiframepreview', post );
  173. deck.on( 'closeoverlay', post );
  174. // Post the initial state
  175. post();
  176. }
  177. return {
  178. id: 'notes',
  179. init: function( reveal ) {
  180. deck = reveal;
  181. if( !/receiver/i.test( window.location.search ) ) {
  182. // If the there's a 'notes' query set, open directly
  183. if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
  184. openSpeakerWindow();
  185. }
  186. else {
  187. // Keep listening for speaker view heartbeats. If we receive a
  188. // heartbeat from an orphaned window, reconnect it. This ensures
  189. // that we remain connected to the notes even if the presentation
  190. // is reloaded.
  191. window.addEventListener( 'message', event => {
  192. if( !speakerWindow && typeof event.data === 'string' ) {
  193. let data;
  194. try {
  195. data = JSON.parse( event.data );
  196. }
  197. catch( error ) {}
  198. if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
  199. reconnectSpeakerWindow( event.source );
  200. }
  201. }
  202. });
  203. }
  204. // Open the notes when the 's' key is hit
  205. deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
  206. openSpeakerWindow();
  207. } );
  208. }
  209. },
  210. open: openSpeakerWindow
  211. };
  212. };
  213. export default Plugin;