Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/formats/revealjs/reveal/plugin/notes/plugin.js
12923 views
1
import speakerViewHTML from './speaker-view.html'
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
213
// Post the initial state
214
post();
215
216
}
217
218
return {
219
id: 'notes',
220
221
init: function( reveal ) {
222
223
deck = reveal;
224
225
if( !/receiver/i.test( window.location.search ) ) {
226
227
// If the there's a 'notes' query set, open directly
228
if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
229
openSpeakerWindow();
230
}
231
else {
232
// Keep listening for speaker view hearbeats. If we receive a
233
// heartbeat from an orphaned window, reconnect it. This ensures
234
// that we remain connected to the notes even if the presentation
235
// is reloaded.
236
window.addEventListener( 'message', event => {
237
238
if( !speakerWindow && typeof event.data === 'string' ) {
239
let data;
240
241
try {
242
data = JSON.parse( event.data );
243
}
244
catch( error ) {}
245
246
if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
247
reconnectSpeakerWindow( event.source );
248
}
249
}
250
});
251
}
252
253
// Open the notes when the 's' key is hit
254
deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
255
openSpeakerWindow();
256
} );
257
258
}
259
260
},
261
262
open: openSpeakerWindow
263
};
264
265
};
266
267
export default Plugin;
268
269