| import speakerViewHTML from './speaker-view.html' |
| |
| import { marked } from 'marked'; |
| |
| /** |
| * Handles opening of and synchronization with the reveal.js |
| * notes window. |
| * |
| * Handshake process: |
| * 1. This window posts 'connect' to notes window |
| * - Includes URL of presentation to show |
| * 2. Notes window responds with 'connected' when it is available |
| * 3. This window proceeds to send the current presentation state |
| * to the notes window |
| */ |
| const Plugin = () => { |
| |
| let connectInterval; |
| let speakerWindow = null; |
| let deck; |
| |
| /** |
| * Opens a new speaker view window. |
| */ |
| function openSpeakerWindow() { |
| |
| // If a window is already open, focus it |
| if( speakerWindow && !speakerWindow.closed ) { |
| speakerWindow.focus(); |
| } |
| else { |
| speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' ); |
| speakerWindow.marked = marked; |
| speakerWindow.document.write( speakerViewHTML ); |
| |
| if( !speakerWindow ) { |
| alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' ); |
| return; |
| } |
| |
| connect(); |
| } |
| |
| } |
| |
| /** |
| * Reconnect with an existing speaker view window. |
| */ |
| function reconnectSpeakerWindow( reconnectWindow ) { |
| |
| if( speakerWindow && !speakerWindow.closed ) { |
| speakerWindow.focus(); |
| } |
| else { |
| speakerWindow = reconnectWindow; |
| window.addEventListener( 'message', onPostMessage ); |
| onConnected(); |
| } |
| |
| } |
| |
| /** |
| * Connect to the notes window through a postmessage handshake. |
| * Using postmessage enables us to work in situations where the |
| * origins differ, such as a presentation being opened from the |
| * file system. |
| */ |
| function connect() { |
| |
| const presentationURL = deck.getConfig().url; |
| |
| const url = typeof presentationURL === 'string' ? presentationURL : |
| window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search; |
| |
| // Keep trying to connect until we get a 'connected' message back |
| connectInterval = setInterval( function() { |
| speakerWindow.postMessage( JSON.stringify( { |
| namespace: 'reveal-notes', |
| type: 'connect', |
| state: deck.getState(), |
| url |
| } ), '*' ); |
| }, 500 ); |
| |
| window.addEventListener( 'message', onPostMessage ); |
| |
| } |
| |
| /** |
| * Calls the specified Reveal.js method with the provided argument |
| * and then pushes the result to the notes frame. |
| */ |
| function callRevealApi( methodName, methodArguments, callId ) { |
| |
| let result = deck[methodName].apply( deck, methodArguments ); |
| speakerWindow.postMessage( JSON.stringify( { |
| namespace: 'reveal-notes', |
| type: 'return', |
| result, |
| callId |
| } ), '*' ); |
| |
| } |
| |
| /** |
| * Posts the current slide data to the notes window. |
| */ |
| function post( event ) { |
| |
| let slideElement = deck.getCurrentSlide(), |
| notesElements = slideElement.querySelectorAll( 'aside.notes' ), |
| fragmentElement = slideElement.querySelector( '.current-fragment' ); |
| |
| let messageData = { |
| namespace: 'reveal-notes', |
| type: 'state', |
| notes: '', |
| markdown: false, |
| whitespace: 'normal', |
| state: deck.getState() |
| }; |
| |
| // Look for notes defined in a slide attribute |
| if( slideElement.hasAttribute( 'data-notes' ) ) { |
| messageData.notes = slideElement.getAttribute( 'data-notes' ); |
| messageData.whitespace = 'pre-wrap'; |
| } |
| |
| // Look for notes defined in a fragment |
| if( fragmentElement ) { |
| let fragmentNotes = fragmentElement.querySelector( 'aside.notes' ); |
| if( fragmentNotes ) { |
| messageData.notes = fragmentNotes.innerHTML; |
| messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string'; |
| |
| // Ignore other slide notes |
| notesElements = null; |
| } |
| else if( fragmentElement.hasAttribute( 'data-notes' ) ) { |
| messageData.notes = fragmentElement.getAttribute( 'data-notes' ); |
| messageData.whitespace = 'pre-wrap'; |
| |
| // In case there are slide notes |
| notesElements = null; |
| } |
| } |
| |
| // Look for notes defined in an aside element |
| if( notesElements && notesElements.length ) { |
| // Ignore notes inside of fragments since those are shown |
| // individually when stepping through fragments |
| notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null ); |
| |
| messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' ); |
| messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string'; |
| } |
| |
| speakerWindow.postMessage( JSON.stringify( messageData ), '*' ); |
| |
| } |
| |
| /** |
| * Check if the given event is from the same origin as the |
| * current window. |
| */ |
| function isSameOriginEvent( event ) { |
| |
| try { |
| return window.location.origin === event.source.location.origin; |
| } |
| catch ( error ) { |
| return false; |
| } |
| |
| } |
| |
| function onPostMessage( event ) { |
| |
| // Only allow same-origin messages |
| // (added 12/5/22 as a XSS safeguard) |
| if( isSameOriginEvent( event ) ) { |
| |
| try { |
| let data = JSON.parse( event.data ); |
| if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { |
| clearInterval( connectInterval ); |
| onConnected(); |
| } |
| else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) { |
| callRevealApi( data.methodName, data.arguments, data.callId ); |
| } |
| } catch (e) {} |
| |
| } |
| |
| } |
| |
| /** |
| * Called once we have established a connection to the notes |
| * window. |
| */ |
| function onConnected() { |
| |
| // Monitor events that trigger a change in state |
| deck.on( 'slidechanged', post ); |
| deck.on( 'fragmentshown', post ); |
| deck.on( 'fragmenthidden', post ); |
| deck.on( 'overviewhidden', post ); |
| deck.on( 'overviewshown', post ); |
| deck.on( 'paused', post ); |
| deck.on( 'resumed', post ); |
| |
| // Post the initial state |
| post(); |
| |
| } |
| |
| return { |
| id: 'notes', |
| |
| init: function( reveal ) { |
| |
| deck = reveal; |
| |
| if( !/receiver/i.test( window.location.search ) ) { |
| |
| // If the there's a 'notes' query set, open directly |
| if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { |
| openSpeakerWindow(); |
| } |
| else { |
| // Keep listening for speaker view hearbeats. If we receive a |
| // heartbeat from an orphaned window, reconnect it. This ensures |
| // that we remain connected to the notes even if the presentation |
| // is reloaded. |
| window.addEventListener( 'message', event => { |
| |
| if( !speakerWindow && typeof event.data === 'string' ) { |
| let data; |
| |
| try { |
| data = JSON.parse( event.data ); |
| } |
| catch( error ) {} |
| |
| if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) { |
| reconnectSpeakerWindow( event.source ); |
| } |
| } |
| }); |
| } |
| |
| // Open the notes when the 's' key is hit |
| deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() { |
| openSpeakerWindow(); |
| } ); |
| |
| } |
| |
| }, |
| |
| open: openSpeakerWindow |
| }; |
| |
| }; |
| |
| export default Plugin; |