blob: 402829371c66bb1412412e76bffa8257a91d2b7a [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import speakerViewHTML from './speaker-view.html';
2
3import 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 */
16const Plugin = () => {
17
18 let popup = null;
19
20 let deck;
21
22 function openNotes() {
23
24 if (popup && !popup.closed) {
25 popup.focus();
26 return;
27 }
28
29 popup = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );
30 popup.marked = marked;
31 popup.document.write( speakerViewHTML );
32
33 if( !popup ) {
34 alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
35 return;
36 }
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 } );
65 }
66
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 ) {
72
73 let result = deck[methodName].apply( deck, methodArguments );
74 popup.postMessage( JSON.stringify( {
75 namespace: 'reveal-notes',
76 type: 'return',
77 result: result,
78 callId: callId
79 } ), '*' );
80
81 }
82
83 /**
84 * Posts the current slide data to the notes window
85 */
86 function post( event ) {
87
88 let slideElement = deck.getCurrentSlide(),
89 notesElement = slideElement.querySelector( 'aside.notes' ),
90 fragmentElement = slideElement.querySelector( '.current-fragment' );
91
92 let messageData = {
93 namespace: 'reveal-notes',
94 type: 'state',
95 notes: '',
96 markdown: false,
97 whitespace: 'normal',
98 state: deck.getState()
99 };
100
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 }
106
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';
116
117 // In case there are slide notes
118 notesElement = null;
119 }
120 }
121
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';
126 }
127
128 popup.postMessage( JSON.stringify( messageData ), '*' );
129
130 }
131
132 /**
133 * Called once we have established a connection to the notes
134 * window.
135 */
136 function onConnected() {
137
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 );
146
147 // Post the initial state
148 post();
149
150 }
151
152 connect();
153
154 }
155
156 return {
157 id: 'notes',
158
159 init: function( reveal ) {
160
161 deck = reveal;
162
163 if( !/receiver/i.test( window.location.search ) ) {
164
165 // If the there's a 'notes' query set, open directly
166 if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
167 openNotes();
168 }
169
170 // Open the notes when the 's' key is hit
171 deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
172 openNotes();
173 } );
174
175 }
176
177 },
178
179 open: openNotes
180 };
181
182};
183
184export default Plugin;