1
0

plugin.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. notesElement = slideElement.querySelector( '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. notesElement = fragmentNotes;
  109. }
  110. else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
  111. messageData.notes = fragmentElement.getAttribute( 'data-notes' );
  112. messageData.whitespace = 'pre-wrap';
  113. // In case there are slide notes
  114. notesElement = null;
  115. }
  116. }
  117. // Look for notes defined in an aside element
  118. if( notesElement ) {
  119. messageData.notes = notesElement.innerHTML;
  120. messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
  121. }
  122. speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
  123. }
  124. function onPostMessage( event ) {
  125. let data = JSON.parse( event.data );
  126. if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
  127. clearInterval( connectInterval );
  128. onConnected();
  129. }
  130. else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
  131. callRevealApi( data.methodName, data.arguments, data.callId );
  132. }
  133. }
  134. /**
  135. * Called once we have established a connection to the notes
  136. * window.
  137. */
  138. function onConnected() {
  139. // Monitor events that trigger a change in state
  140. deck.on( 'slidechanged', post );
  141. deck.on( 'fragmentshown', post );
  142. deck.on( 'fragmenthidden', post );
  143. deck.on( 'overviewhidden', post );
  144. deck.on( 'overviewshown', post );
  145. deck.on( 'paused', post );
  146. deck.on( 'resumed', post );
  147. // Post the initial state
  148. post();
  149. }
  150. return {
  151. id: 'notes',
  152. init: function( reveal ) {
  153. deck = reveal;
  154. if( !/receiver/i.test( window.location.search ) ) {
  155. // If the there's a 'notes' query set, open directly
  156. if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
  157. openSpeakerWindow();
  158. }
  159. else {
  160. // Keep listening for speaker view hearbeats. If we receive a
  161. // heartbeat from an orphaned window, reconnect it. This ensures
  162. // that we remain connected to the notes even if the presentation
  163. // is reloaded.
  164. window.addEventListener( 'message', event => {
  165. if( !speakerWindow && typeof event.data === 'string' ) {
  166. let data;
  167. try {
  168. data = JSON.parse( event.data );
  169. }
  170. catch( error ) {}
  171. if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
  172. reconnectSpeakerWindow( event.source );
  173. }
  174. }
  175. });
  176. }
  177. // Open the notes when the 's' key is hit
  178. deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
  179. openSpeakerWindow();
  180. } );
  181. }
  182. },
  183. open: openSpeakerWindow
  184. };
  185. };
  186. export default Plugin;