Skip to content

Commit

Permalink
fiv out of sync speaker view after presentation reloads #2822 #3032
Browse files Browse the repository at this point in the history
  • Loading branch information
hakimel committed Feb 10, 2022
1 parent 6b53532 commit ff20051
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 112 deletions.
2 changes: 1 addition & 1 deletion plugin/notes/notes.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugin/notes/notes.js

Large diffs are not rendered by default.

260 changes: 150 additions & 110 deletions plugin/notes/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,141 +15,167 @@ import marked from 'marked';
*/
const Plugin = () => {

let popup = null;

let deck;
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;
}

function openNotes() {
connect();
}

if (popup && !popup.closed) {
popup.focus();
return;
}
}

popup = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
popup.marked = marked;
popup.document.write( speakerViewHTML );
/**
* Reconnect with an existing speaker view window.
*/
function reconnectSpeakerWindow( reconnectWindow ) {

if( !popup ) {
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
return;
if( speakerWindow && !speakerWindow.closed ) {
speakerWindow.focus();
}

/**
* 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 );
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
clearInterval( connectInterval );
onConnected();
}
if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
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',
type: 'return',
result: result,
callId: callId
type: 'connect',
url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
state: deck.getState()
} ), '*' );
}, 500 );

}
window.addEventListener( 'message', onPostMessage );

/**
* 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' );
/**
* 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 messageData = {
namespace: 'reveal-notes',
type: 'state',
notes: '',
markdown: false,
whitespace: 'normal',
state: deck.getState()
};
let result = deck[methodName].apply( deck, methodArguments );
speakerWindow.postMessage( JSON.stringify( {
namespace: 'reveal-notes',
type: 'return',
result,
callId
} ), '*' );

// 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';
/**
* 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';
}

// 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';

// Look for notes defined in an aside element
if( notesElement ) {
messageData.notes = notesElement.innerHTML;
messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
// In case there are slide notes
notesElement = null;
}
}

popup.postMessage( JSON.stringify( messageData ), '*' );

// Look for notes defined in an aside element
if( notesElement ) {
messageData.notes = notesElement.innerHTML;
messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
}

/**
* Called once we have established a connection to the notes
* window.
*/
function onConnected() {
speakerWindow.postMessage( JSON.stringify( messageData ), '*' );

// 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();
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 );
}

connect();
}

/**
* 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();

}

Expand All @@ -164,19 +190,33 @@ const Plugin = () => {

// If the there's a 'notes' query set, open directly
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
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
openNotes();
openSpeakerWindow();
} );

}

},

open: openNotes
open: openSpeakerWindow
};

};
Expand Down
13 changes: 13 additions & 0 deletions plugin/notes/speaker-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ <h4 class="label">Notes</h4>
setupKeyboard();
setupNotes();
setupTimer();
setupHeartbeat();
}

}
Expand Down Expand Up @@ -536,6 +537,18 @@ <h4 class="label">Notes</h4>

}

/**
* 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 ) {

callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {
Expand Down

0 comments on commit ff20051

Please sign in to comment.