@@ -15,141 +15,167 @@ import marked from 'marked';
1515 */
1616const 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 ( / ( \? | \& ) n o t e s / 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} ;
0 commit comments