Skip to content

Commit ff20051

Browse files
committed
fiv out of sync speaker view after presentation reloads #2822 #3032
1 parent 6b53532 commit ff20051

4 files changed

Lines changed: 165 additions & 112 deletions

File tree

plugin/notes/notes.esm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/notes/notes.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/notes/plugin.js

Lines changed: 150 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -15,141 +15,167 @@ import marked from 'marked';
1515
*/
1616
const Plugin = () => {
1717

18-
let popup = null;
19-
20-
let deck;
18+
let connectInterval;
19+
let speakerWindow = null;
20+
let deck;
21+
22+
/**
23+
* Opens a new speaker view window.
24+
*/
25+
function openSpeakerWindow() {
26+
27+
// If a window is already open, focus it
28+
if( speakerWindow && !speakerWindow.closed ) {
29+
speakerWindow.focus();
30+
}
31+
else {
32+
speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
33+
speakerWindow.marked = marked;
34+
speakerWindow.document.write( speakerViewHTML );
35+
36+
if( !speakerWindow ) {
37+
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
38+
return;
39+
}
2140

22-
function openNotes() {
41+
connect();
42+
}
2343

24-
if (popup && !popup.closed) {
25-
popup.focus();
26-
return;
27-
}
44+
}
2845

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

33-
if( !popup ) {
34-
alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
35-
return;
51+
if( speakerWindow && !speakerWindow.closed ) {
52+
speakerWindow.focus();
3653
}
37-
38-
/**
39-
* Connect to the notes window through a postmessage handshake.
40-
* Using postmessage enables us to work in situations where the
41-
* origins differ, such as a presentation being opened from the
42-
* file system.
43-
*/
44-
function connect() {
45-
// Keep trying to connect until we get a 'connected' message back
46-
let connectInterval = setInterval( function() {
47-
popup.postMessage( JSON.stringify( {
48-
namespace: 'reveal-notes',
49-
type: 'connect',
50-
url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
51-
state: deck.getState()
52-
} ), '*' );
53-
}, 500 );
54-
55-
window.addEventListener( 'message', function( event ) {
56-
let data = JSON.parse( event.data );
57-
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
58-
clearInterval( connectInterval );
59-
onConnected();
60-
}
61-
if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
62-
callRevealApi( data.methodName, data.arguments, data.callId );
63-
}
64-
} );
54+
else {
55+
speakerWindow = reconnectWindow;
56+
window.addEventListener( 'message', onPostMessage );
57+
onConnected();
6558
}
6659

67-
/**
68-
* Calls the specified Reveal.js method with the provided argument
69-
* and then pushes the result to the notes frame.
70-
*/
71-
function callRevealApi( methodName, methodArguments, callId ) {
60+
}
7261

73-
let result = deck[methodName].apply( deck, methodArguments );
74-
popup.postMessage( JSON.stringify( {
62+
/**
63+
* Connect to the notes window through a postmessage handshake.
64+
* Using postmessage enables us to work in situations where the
65+
* origins differ, such as a presentation being opened from the
66+
* file system.
67+
*/
68+
function connect() {
69+
70+
// Keep trying to connect until we get a 'connected' message back
71+
connectInterval = setInterval( function() {
72+
speakerWindow.postMessage( JSON.stringify( {
7573
namespace: 'reveal-notes',
76-
type: 'return',
77-
result: result,
78-
callId: callId
74+
type: 'connect',
75+
url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
76+
state: deck.getState()
7977
} ), '*' );
78+
}, 500 );
8079

81-
}
80+
window.addEventListener( 'message', onPostMessage );
8281

83-
/**
84-
* Posts the current slide data to the notes window
85-
*/
86-
function post( event ) {
82+
}
8783

88-
let slideElement = deck.getCurrentSlide(),
89-
notesElement = slideElement.querySelector( 'aside.notes' ),
90-
fragmentElement = slideElement.querySelector( '.current-fragment' );
84+
/**
85+
* Calls the specified Reveal.js method with the provided argument
86+
* and then pushes the result to the notes frame.
87+
*/
88+
function callRevealApi( methodName, methodArguments, callId ) {
9189

92-
let messageData = {
93-
namespace: 'reveal-notes',
94-
type: 'state',
95-
notes: '',
96-
markdown: false,
97-
whitespace: 'normal',
98-
state: deck.getState()
99-
};
90+
let result = deck[methodName].apply( deck, methodArguments );
91+
speakerWindow.postMessage( JSON.stringify( {
92+
namespace: 'reveal-notes',
93+
type: 'return',
94+
result,
95+
callId
96+
} ), '*' );
10097

101-
// Look for notes defined in a slide attribute
102-
if( slideElement.hasAttribute( 'data-notes' ) ) {
103-
messageData.notes = slideElement.getAttribute( 'data-notes' );
104-
messageData.whitespace = 'pre-wrap';
105-
}
98+
}
10699

107-
// Look for notes defined in a fragment
108-
if( fragmentElement ) {
109-
let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
110-
if( fragmentNotes ) {
111-
notesElement = fragmentNotes;
112-
}
113-
else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
114-
messageData.notes = fragmentElement.getAttribute( 'data-notes' );
115-
messageData.whitespace = 'pre-wrap';
100+
/**
101+
* Posts the current slide data to the notes window.
102+
*/
103+
function post( event ) {
104+
105+
let slideElement = deck.getCurrentSlide(),
106+
notesElement = slideElement.querySelector( 'aside.notes' ),
107+
fragmentElement = slideElement.querySelector( '.current-fragment' );
108+
109+
let messageData = {
110+
namespace: 'reveal-notes',
111+
type: 'state',
112+
notes: '',
113+
markdown: false,
114+
whitespace: 'normal',
115+
state: deck.getState()
116+
};
117+
118+
// Look for notes defined in a slide attribute
119+
if( slideElement.hasAttribute( 'data-notes' ) ) {
120+
messageData.notes = slideElement.getAttribute( 'data-notes' );
121+
messageData.whitespace = 'pre-wrap';
122+
}
116123

117-
// In case there are slide notes
118-
notesElement = null;
119-
}
124+
// Look for notes defined in a fragment
125+
if( fragmentElement ) {
126+
let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
127+
if( fragmentNotes ) {
128+
notesElement = fragmentNotes;
120129
}
130+
else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
131+
messageData.notes = fragmentElement.getAttribute( 'data-notes' );
132+
messageData.whitespace = 'pre-wrap';
121133

122-
// Look for notes defined in an aside element
123-
if( notesElement ) {
124-
messageData.notes = notesElement.innerHTML;
125-
messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
134+
// In case there are slide notes
135+
notesElement = null;
126136
}
137+
}
127138

128-
popup.postMessage( JSON.stringify( messageData ), '*' );
129-
139+
// Look for notes defined in an aside element
140+
if( notesElement ) {
141+
messageData.notes = notesElement.innerHTML;
142+
messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
130143
}
131144

132-
/**
133-
* Called once we have established a connection to the notes
134-
* window.
135-
*/
136-
function onConnected() {
145+
speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
137146

138-
// Monitor events that trigger a change in state
139-
deck.on( 'slidechanged', post );
140-
deck.on( 'fragmentshown', post );
141-
deck.on( 'fragmenthidden', post );
142-
deck.on( 'overviewhidden', post );
143-
deck.on( 'overviewshown', post );
144-
deck.on( 'paused', post );
145-
deck.on( 'resumed', post );
147+
}
146148

147-
// Post the initial state
148-
post();
149+
function onPostMessage( event ) {
149150

151+
let data = JSON.parse( event.data );
152+
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
153+
clearInterval( connectInterval );
154+
onConnected();
155+
}
156+
else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
157+
callRevealApi( data.methodName, data.arguments, data.callId );
150158
}
151159

152-
connect();
160+
}
161+
162+
/**
163+
* Called once we have established a connection to the notes
164+
* window.
165+
*/
166+
function onConnected() {
167+
168+
// Monitor events that trigger a change in state
169+
deck.on( 'slidechanged', post );
170+
deck.on( 'fragmentshown', post );
171+
deck.on( 'fragmenthidden', post );
172+
deck.on( 'overviewhidden', post );
173+
deck.on( 'overviewshown', post );
174+
deck.on( 'paused', post );
175+
deck.on( 'resumed', post );
176+
177+
// Post the initial state
178+
post();
153179

154180
}
155181

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

165191
// If the there's a 'notes' query set, open directly
166192
if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
167-
openNotes();
193+
openSpeakerWindow();
194+
}
195+
else {
196+
// Keep listening for speaker view hearbeats. If we receive a
197+
// heartbeat from an orphaned window, reconnect it. This ensures
198+
// that we remain connected to the notes even if the presentation
199+
// is reloaded.
200+
window.addEventListener( 'message', event => {
201+
if( !speakerWindow ) {
202+
let data = JSON.parse( event.data );
203+
if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
204+
reconnectSpeakerWindow( event.source );
205+
}
206+
}
207+
});
168208
}
169209

170210
// Open the notes when the 's' key is hit
171211
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
172-
openNotes();
212+
openSpeakerWindow();
173213
} );
174214

175215
}
176216

177217
},
178218

179-
open: openNotes
219+
open: openSpeakerWindow
180220
};
181221

182222
};

plugin/notes/speaker-view.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ <h4 class="label">Notes</h4>
435435
setupKeyboard();
436436
setupNotes();
437437
setupTimer();
438+
setupHeartbeat();
438439
}
439440

440441
}
@@ -536,6 +537,18 @@ <h4 class="label">Notes</h4>
536537

537538
}
538539

540+
/**
541+
* We send out a heartbeat at all times to ensure we can
542+
* reconnect with the main presentation window after reloads.
543+
*/
544+
function setupHeartbeat() {
545+
546+
setInterval( () => {
547+
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'heartbeat'} ), '*' );
548+
}, 1000 );
549+
550+
}
551+
539552
function getTimings( callback ) {
540553

541554
callRevealApi( 'getSlidesAttributes', [], function ( slideAttributes ) {

0 commit comments

Comments
 (0)