1
0

plugin.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. // Keep trying to connect until we get a 'connected' message back
  58. connectInterval = setInterval( function() {
  59. speakerWindow.postMessage( JSON.stringify( {
  60. namespace: 'reveal-notes',
  61. type: 'connect',
  62. url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
  63. state: deck.getState()
  64. } ), '*' );
  65. }, 500 );
  66. window.addEventListener( 'message', onPostMessage );
  67. }
  68. /**
  69. * Calls the specified Reveal.js method with the provided argument
  70. * and then pushes the result to the notes frame.
  71. */
  72. function callRevealApi( methodName, methodArguments, callId ) {
  73. let result = deck[methodName].apply( deck, methodArguments );
  74. speakerWindow.postMessage( JSON.stringify( {
  75. namespace: 'reveal-notes',
  76. type: 'return',
  77. result,
  78. callId
  79. } ), '*' );
  80. }
  81. /**
  82. * Posts the current slide data to the notes window.
  83. */
  84. function post( event ) {
  85. let slideElement = deck.getCurrentSlide(),
  86. notesElement = slideElement.querySelector( 'aside.notes' ),
  87. fragmentElement = slideElement.querySelector( '.current-fragment' );
  88. let messageData = {
  89. namespace: 'reveal-notes',
  90. type: 'state',
  91. notes: '',
  92. markdown: false,
  93. whitespace: 'normal',
  94. state: deck.getState()
  95. };
  96. // Look for notes defined in a slide attribute
  97. if( slideElement.hasAttribute( 'data-notes' ) ) {
  98. messageData.notes = slideElement.getAttribute( 'data-notes' );
  99. messageData.whitespace = 'pre-wrap';
  100. }
  101. // Look for notes defined in a fragment
  102. if( fragmentElement ) {
  103. let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
  104. if( fragmentNotes ) {
  105. notesElement = fragmentNotes;
  106. }
  107. else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
  108. messageData.notes = fragmentElement.getAttribute( 'data-notes' );
  109. messageData.whitespace = 'pre-wrap';
  110. // In case there are slide notes
  111. notesElement = null;
  112. }
  113. }
  114. // Look for notes defined in an aside element
  115. if( notesElement ) {
  116. messageData.notes = notesElement.innerHTML;
  117. messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
  118. }
  119. speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
  120. }
  121. function onPostMessage( event ) {
  122. let data = JSON.parse( event.data );
  123. if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
  124. clearInterval( connectInterval );
  125. onConnected();
  126. }
  127. else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
  128. callRevealApi( data.methodName, data.arguments, data.callId );
  129. }
  130. }
  131. /**
  132. * Called once we have established a connection to the notes
  133. * window.
  134. */
  135. function onConnected() {
  136. // Monitor events that trigger a change in state
  137. deck.on( 'slidechanged', post );
  138. deck.on( 'fragmentshown', post );
  139. deck.on( 'fragmenthidden', post );
  140. deck.on( 'overviewhidden', post );
  141. deck.on( 'overviewshown', post );
  142. deck.on( 'paused', post );
  143. deck.on( 'resumed', post );
  144. // Post the initial state
  145. post();
  146. }
  147. return {
  148. id: 'notes',
  149. init: function( reveal ) {
  150. deck = reveal;
  151. if( !/receiver/i.test( window.location.search ) ) {
  152. // If the there's a 'notes' query set, open directly
  153. if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
  154. openSpeakerWindow();
  155. }
  156. else {
  157. // Keep listening for speaker view hearbeats. If we receive a
  158. // heartbeat from an orphaned window, reconnect it. This ensures
  159. // that we remain connected to the notes even if the presentation
  160. // is reloaded.
  161. window.addEventListener( 'message', event => {
  162. if( !speakerWindow ) {
  163. let data = JSON.parse( event.data );
  164. if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
  165. reconnectSpeakerWindow( event.source );
  166. }
  167. }
  168. });
  169. }
  170. // Open the notes when the 's' key is hit
  171. deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
  172. openSpeakerWindow();
  173. } );
  174. }
  175. },
  176. open: openSpeakerWindow
  177. };
  178. };
  179. export default Plugin;