返回 reveal.js
plugin.js
根目录 / plugin / notes / plugin.js
1 import speakerViewHTML from './speaker-view.html?raw'
2
3 import { marked } from 'marked';
4
5 /**
6 * Handles opening of and synchronization with the reveal.js
7 * notes window.
8 *
9 * Handshake process:
10 * 1. This window posts 'connect' to notes window
11 * - Includes URL of presentation to show
12 * 2. Notes window responds with 'connected' when it is available
13 * 3. This window proceeds to send the current presentation state
14 * to the notes window
15 */
16 const Plugin = () => {
17
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 }
40
41 connect();
42 }
43
44 }
45
46 /**
47 * Reconnect with an existing speaker view window.
48 */
49 function reconnectSpeakerWindow( reconnectWindow ) {
50
51 if( speakerWindow && !speakerWindow.closed ) {
52 speakerWindow.focus();
53 }
54 else {
55 speakerWindow = reconnectWindow;
56 window.addEventListener( 'message', onPostMessage );
57 onConnected();
58 }
59
60 }
61
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 const presentationURL = deck.getConfig().url;
71
72 const url = typeof presentationURL === 'string' ? presentationURL :
73 window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;
74
75 // Keep trying to connect until we get a 'connected' message back
76 connectInterval = setInterval( function() {
77 speakerWindow.postMessage( JSON.stringify( {
78 namespace: 'reveal-notes',
79 type: 'connect',
80 state: deck.getState(),
81 url
82 } ), '*' );
83 }, 500 );
84
85 window.addEventListener( 'message', onPostMessage );
86
87 }
88
89 /**
90 * Calls the specified Reveal.js method with the provided argument
91 * and then pushes the result to the notes frame.
92 */
93 function callRevealApi( methodName, methodArguments, callId ) {
94
95 let result = deck[methodName].apply( deck, methodArguments );
96 speakerWindow.postMessage( JSON.stringify( {
97 namespace: 'reveal-notes',
98 type: 'return',
99 result,
100 callId
101 } ), '*' );
102
103 }
104
105 /**
106 * Posts the current slide data to the notes window.
107 */
108 function post( event ) {
109
110 let slideElement = deck.getCurrentSlide(),
111 notesElements = slideElement.querySelectorAll( 'aside.notes' ),
112 fragmentElement = slideElement.querySelector( '.current-fragment' );
113
114 let messageData = {
115 namespace: 'reveal-notes',
116 type: 'state',
117 notes: '',
118 markdown: false,
119 whitespace: 'normal',
120 state: deck.getState()
121 };
122
123 // Look for notes defined in a slide attribute
124 if( slideElement.hasAttribute( 'data-notes' ) ) {
125 messageData.notes = slideElement.getAttribute( 'data-notes' );
126 messageData.whitespace = 'pre-wrap';
127 }
128
129 // Look for notes defined in a fragment
130 if( fragmentElement ) {
131 let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
132 if( fragmentNotes ) {
133 messageData.notes = fragmentNotes.innerHTML;
134 messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';
135
136 // Ignore other slide notes
137 notesElements = null;
138 }
139 else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
140 messageData.notes = fragmentElement.getAttribute( 'data-notes' );
141 messageData.whitespace = 'pre-wrap';
142
143 // In case there are slide notes
144 notesElements = null;
145 }
146 }
147
148 // Look for notes defined in an aside element
149 if( notesElements && notesElements.length ) {
150 // Ignore notes inside of fragments since those are shown
151 // individually when stepping through fragments
152 notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );
153
154 messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );
155 messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
156 }
157
158 speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
159
160 }
161
162 /**
163 * Check if the given event is from the same origin as the
164 * current window.
165 */
166 function isSameOriginEvent( event ) {
167
168 try {
169 return window.location.origin === event.source.location.origin;
170 }
171 catch ( error ) {
172 return false;
173 }
174
175 }
176
177 function onPostMessage( event ) {
178
179 // Only allow same-origin messages
180 // (added 12/5/22 as a XSS safeguard)
181 if( isSameOriginEvent( event ) ) {
182
183 try {
184 let data = JSON.parse( event.data );
185 if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
186 clearInterval( connectInterval );
187 onConnected();
188 }
189 else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
190 callRevealApi( data.methodName, data.arguments, data.callId );
191 }
192 } catch (e) {}
193
194 }
195
196 }
197
198 /**
199 * Called once we have established a connection to the notes
200 * window.
201 */
202 function onConnected() {
203
204 // Monitor events that trigger a change in state
205 deck.on( 'slidechanged', post );
206 deck.on( 'fragmentshown', post );
207 deck.on( 'fragmenthidden', post );
208 deck.on( 'overviewhidden', post );
209 deck.on( 'overviewshown', post );
210 deck.on( 'paused', post );
211 deck.on( 'resumed', post );
212 deck.on( 'previewiframe', post );
213 deck.on( 'previewimage', post );
214 deck.on( 'previewvideo', post );
215 deck.on( 'closeoverlay', post );
216
217 // Post the initial state
218 post();
219
220 }
221
222 return {
223 id: 'notes',
224
225 init: function( reveal ) {
226
227 deck = reveal;
228
229 if( !/receiver/i.test( window.location.search ) ) {
230
231 // If the there's a 'notes' query set, open directly
232 if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
233 openSpeakerWindow();
234 }
235 else {
236 // Keep listening for speaker view heartbeats. If we receive a
237 // heartbeat from an orphaned window, reconnect it. This ensures
238 // that we remain connected to the notes even if the presentation
239 // is reloaded.
240 window.addEventListener( 'message', event => {
241
242 if( !speakerWindow && typeof event.data === 'string' ) {
243 let data;
244
245 try {
246 data = JSON.parse( event.data );
247 }
248 catch( error ) {}
249
250 if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
251 reconnectSpeakerWindow( event.source );
252 }
253 }
254 });
255 }
256
257 // Open the notes when the 's' key is hit
258 deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
259 openSpeakerWindow();
260 } );
261
262 }
263
264 },
265
266 open: openSpeakerWindow
267 };
268
269 };
270
271 export default Plugin;
272
272 lines JAVASCRIPT