Spaces:
Running
Running
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(), | |
notesElement = slideElement.querySelector( '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 ) { | |
notesElement = fragmentNotes; | |
} | |
else if( fragmentElement.hasAttribute( 'data-notes' ) ) { | |
messageData.notes = fragmentElement.getAttribute( 'data-notes' ); | |
messageData.whitespace = 'pre-wrap'; | |
// In case there are slide notes | |
notesElement = null; | |
} | |
} | |
// Look for notes defined in an aside element | |
if( notesElement ) { | |
messageData.notes = notesElement.innerHTML; | |
messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string'; | |
} | |
speakerWindow.postMessage( JSON.stringify( messageData ), '*' ); | |
} | |
function onPostMessage( event ) { | |
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 ); | |
} | |
} | |
/** | |
* 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; | |