fiv out of sync speaker view after presentation reloads #2822 #3032

This commit is contained in:
hakimel 2022-02-10 13:28:37 +01:00
parent 6b535328c0
commit ff20051861
4 changed files with 174 additions and 121 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,141 +15,167 @@ import marked from 'marked';
*/ */
const Plugin = () => { const Plugin = () => {
let popup = null; let connectInterval;
let speakerWindow = null;
let deck;
let deck; /**
* Opens a new speaker view window.
*/
function openSpeakerWindow() {
function openNotes() { // 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 (popup && !popup.closed) { if( !speakerWindow ) {
popup.focus(); alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
return; return;
} }
popup = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' ); connect();
popup.marked = marked;
popup.document.write( speakerViewHTML );
if( !popup ) {
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
return;
} }
/** }
* 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() {
// Keep trying to connect until we get a 'connected' message back
let connectInterval = setInterval( function() {
popup.postMessage( JSON.stringify( {
namespace: 'reveal-notes',
type: 'connect',
url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
state: deck.getState()
} ), '*' );
}, 500 );
window.addEventListener( 'message', function( event ) { /**
let data = JSON.parse( event.data ); * Reconnect with an existing speaker view window.
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { */
clearInterval( connectInterval ); function reconnectSpeakerWindow( reconnectWindow ) {
onConnected();
} if( speakerWindow && !speakerWindow.closed ) {
if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) { speakerWindow.focus();
callRevealApi( data.methodName, data.arguments, data.callId ); }
} else {
} ); speakerWindow = reconnectWindow;
window.addEventListener( 'message', onPostMessage );
onConnected();
} }
/** }
* 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 ); /**
popup.postMessage( JSON.stringify( { * 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() {
// Keep trying to connect until we get a 'connected' message back
connectInterval = setInterval( function() {
speakerWindow.postMessage( JSON.stringify( {
namespace: 'reveal-notes', namespace: 'reveal-notes',
type: 'return', type: 'connect',
result: result, url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
callId: 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() state: deck.getState()
}; } ), '*' );
}, 500 );
// Look for notes defined in a slide attribute window.addEventListener( 'message', onPostMessage );
if( slideElement.hasAttribute( 'data-notes' ) ) {
messageData.notes = slideElement.getAttribute( 'data-notes' ); }
/**
* 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'; messageData.whitespace = 'pre-wrap';
// In case there are slide notes
notesElement = null;
} }
// 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';
}
popup.postMessage( JSON.stringify( messageData ), '*' );
} }
/** // Look for notes defined in an aside element
* Called once we have established a connection to the notes if( notesElement ) {
* window. messageData.notes = notesElement.innerHTML;
*/ messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
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();
} }
connect(); 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();
} }
@ -164,19 +190,33 @@ const Plugin = () => {
// If the there's a 'notes' query set, open directly // If the there's a 'notes' query set, open directly
if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
openNotes(); 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 ) {
let data = JSON.parse( event.data );
if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
reconnectSpeakerWindow( event.source );
}
}
});
} }
// Open the notes when the 's' key is hit // Open the notes when the 's' key is hit
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() { deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
openNotes(); openSpeakerWindow();
} ); } );
} }
}, },
open: openNotes open: openSpeakerWindow
}; };
}; };

View File

@ -435,6 +435,7 @@
setupKeyboard(); setupKeyboard();
setupNotes(); setupNotes();
setupTimer(); setupTimer();
setupHeartbeat();
} }
} }
@ -536,6 +537,18 @@
} }
/**
* We send out a heartbeat at all times to ensure we can
* reconnect with the main presentation window after reloads.
*/
function setupHeartbeat() {
setInterval( () => {
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
}, 1000 );
}
function getTimings( callback ) { function getTimings( callback ) {
callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) { callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {