all repos — slides @ fd8c9cfab9a3969c5b3d87d9724ed0d645b041d9

Reveal-md slides I made for various occasions

réalise/_static/plugin/notes/plugin.js (view raw)

  1import 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 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 ) {
150			messageData.notes = Array.from(notesElements).map( notesElement => notesElement.innerHTML ).join( '\n' );
151			messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';
152		}
153
154		speakerWindow.postMessage( JSON.stringify( messageData ), '*' );
155
156	}
157
158	/**
159	 * Check if the given event is from the same origin as the
160	 * current window.
161	 */
162	function isSameOriginEvent( event ) {
163
164		try {
165			return window.location.origin === event.source.location.origin;
166		}
167		catch ( error ) {
168			return false;
169		}
170
171	}
172
173	function onPostMessage( event ) {
174
175		// Only allow same-origin messages
176		// (added 12/5/22 as a XSS safeguard)
177		if( isSameOriginEvent( event ) ) {
178
179			let data = JSON.parse( event.data );
180			if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
181				clearInterval( connectInterval );
182				onConnected();
183			}
184			else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
185				callRevealApi( data.methodName, data.arguments, data.callId );
186			}
187
188		}
189
190	}
191
192	/**
193	 * Called once we have established a connection to the notes
194	 * window.
195	 */
196	function onConnected() {
197
198		// Monitor events that trigger a change in state
199		deck.on( 'slidechanged', post );
200		deck.on( 'fragmentshown', post );
201		deck.on( 'fragmenthidden', post );
202		deck.on( 'overviewhidden', post );
203		deck.on( 'overviewshown', post );
204		deck.on( 'paused', post );
205		deck.on( 'resumed', post );
206
207		// Post the initial state
208		post();
209
210	}
211
212	return {
213		id: 'notes',
214
215		init: function( reveal ) {
216
217			deck = reveal;
218
219			if( !/receiver/i.test( window.location.search ) ) {
220
221				// If the there's a 'notes' query set, open directly
222				if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
223					openSpeakerWindow();
224				}
225				else {
226					// Keep listening for speaker view hearbeats. If we receive a
227					// heartbeat from an orphaned window, reconnect it. This ensures
228					// that we remain connected to the notes even if the presentation
229					// is reloaded.
230					window.addEventListener( 'message', event => {
231
232						if( !speakerWindow && typeof event.data === 'string' ) {
233							let data;
234
235							try {
236								data = JSON.parse( event.data );
237							}
238							catch( error ) {}
239
240							if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {
241								reconnectSpeakerWindow( event.source );
242							}
243						}
244					});
245				}
246
247				// Open the notes when the 's' key is hit
248				deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
249					openSpeakerWindow();
250				} );
251
252			}
253
254		},
255
256		open: openSpeakerWindow
257	};
258
259};
260
261export default Plugin;