diff options
Diffstat (limited to 'present/plugin/notes/plugin.js')
| -rw-r--r-- | present/plugin/notes/plugin.js | 261 | 
1 files changed, 261 insertions, 0 deletions
| diff --git a/present/plugin/notes/plugin.js b/present/plugin/notes/plugin.js new file mode 100644 index 0000000..ecbf4ad --- /dev/null +++ b/present/plugin/notes/plugin.js @@ -0,0 +1,261 @@ +import speakerViewHTML from './speaker-view.html' + +import { marked } from 'marked'; + +/** + * Handles opening of and synchronization with the reveal.js + * notes window. + * + * Handshake process: + * 1. This window posts 'connect' to notes window + *    - Includes URL of presentation to show + * 2. Notes window responds with 'connected' when it is available + * 3. This window proceeds to send the current presentation state + *    to the notes window + */ +const Plugin = () => { + +	let connectInterval; +	let speakerWindow = null; +	let deck; + +	/** +	 * Opens a new speaker view window. +	 */ +	function openSpeakerWindow() { + +		// If a window is already open, focus it +		if( speakerWindow && !speakerWindow.closed ) { +			speakerWindow.focus(); +		} +		else { +			speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' ); +			speakerWindow.marked = marked; +			speakerWindow.document.write( speakerViewHTML ); + +			if( !speakerWindow ) { +				alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' ); +				return; +			} + +			connect(); +		} + +	} + +	/** +	 * Reconnect with an existing speaker view window. +	 */ +	function reconnectSpeakerWindow( reconnectWindow ) { + +		if( speakerWindow && !speakerWindow.closed ) { +			speakerWindow.focus(); +		} +		else { +			speakerWindow = reconnectWindow; +			window.addEventListener( 'message', onPostMessage ); +			onConnected(); +		} + +	} + +	/** +		* Connect to the notes window through a postmessage handshake. +		* Using postmessage enables us to work in situations where the +		* origins differ, such as a presentation being opened from the +		* file system. +		*/ +	function connect() { + +		const presentationURL = deck.getConfig().url; + +		const url = typeof presentationURL === 'string' ? presentationURL : +								window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search; + +		// Keep trying to connect until we get a 'connected' message back +		connectInterval = setInterval( function() { +			speakerWindow.postMessage( JSON.stringify( { +				namespace: 'reveal-notes', +				type: 'connect', +				state: deck.getState(), +				url +			} ), '*' ); +		}, 500 ); + +		window.addEventListener( 'message', onPostMessage ); + +	} + +	/** +	 * Calls the specified Reveal.js method with the provided argument +	 * and then pushes the result to the notes frame. +	 */ +	function callRevealApi( methodName, methodArguments, callId ) { + +		let result = deck[methodName].apply( deck, methodArguments ); +		speakerWindow.postMessage( JSON.stringify( { +			namespace: 'reveal-notes', +			type: 'return', +			result, +			callId +		} ), '*' ); + +	} + +	/** +	 * Posts the current slide data to the notes window. +	 */ +	function post( event ) { + +		let slideElement = deck.getCurrentSlide(), +			notesElements = slideElement.querySelectorAll( 'aside.notes' ), +			fragmentElement = slideElement.querySelector( '.current-fragment' ); + +		let messageData = { +			namespace: 'reveal-notes', +			type: 'state', +			notes: '', +			markdown: false, +			whitespace: 'normal', +			state: deck.getState() +		}; + +		// Look for notes defined in a slide attribute +		if( slideElement.hasAttribute( 'data-notes' ) ) { +			messageData.notes = slideElement.getAttribute( 'data-notes' ); +			messageData.whitespace = 'pre-wrap'; +		} + +		// Look for notes defined in a fragment +		if( fragmentElement ) { +			let fragmentNotes = fragmentElement.querySelector( 'aside.notes' ); +			if( fragmentNotes ) { +				messageData.notes = fragmentNotes.innerHTML; +				messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string'; + +				// Ignore other slide notes +				notesElements = null; +			} +			else if( fragmentElement.hasAttribute( 'data-notes' ) ) { +				messageData.notes = fragmentElement.getAttribute( 'data-notes' ); +				messageData.whitespace = 'pre-wrap'; + +				// In case there are slide notes +				notesElements = null; +			} +		} + +		// Look for notes defined in an aside element +		if( notesElements ) { +			messageData.notes = Array.from(notesElements).map( notesElement => notesElement.innerHTML ).join( '\n' ); +			messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string'; +		} + +		speakerWindow.postMessage( JSON.stringify( messageData ), '*' ); + +	} + +	/** +	 * Check if the given event is from the same origin as the +	 * current window. +	 */ +	function isSameOriginEvent( event ) { + +		try { +			return window.location.origin === event.source.location.origin; +		} +		catch ( error ) { +			return false; +		} + +	} + +	function onPostMessage( event ) { + +		// Only allow same-origin messages +		// (added 12/5/22 as a XSS safeguard) +		if( isSameOriginEvent( event ) ) { + +			let data = JSON.parse( event.data ); +			if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { +				clearInterval( connectInterval ); +				onConnected(); +			} +			else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) { +				callRevealApi( data.methodName, data.arguments, data.callId ); +			} + +		} + +	} + +	/** +	 * Called once we have established a connection to the notes +	 * window. +	 */ +	function onConnected() { + +		// Monitor events that trigger a change in state +		deck.on( 'slidechanged', post ); +		deck.on( 'fragmentshown', post ); +		deck.on( 'fragmenthidden', post ); +		deck.on( 'overviewhidden', post ); +		deck.on( 'overviewshown', post ); +		deck.on( 'paused', post ); +		deck.on( 'resumed', post ); + +		// Post the initial state +		post(); + +	} + +	return { +		id: 'notes', + +		init: function( reveal ) { + +			deck = reveal; + +			if( !/receiver/i.test( window.location.search ) ) { + +				// If the there's a 'notes' query set, open directly +				if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { +					openSpeakerWindow(); +				} +				else { +					// Keep listening for speaker view hearbeats. If we receive a +					// heartbeat from an orphaned window, reconnect it. This ensures +					// that we remain connected to the notes even if the presentation +					// is reloaded. +					window.addEventListener( 'message', event => { + +						if( !speakerWindow && typeof event.data === 'string' ) { +							let data; + +							try { +								data = JSON.parse( event.data ); +							} +							catch( error ) {} + +							if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) { +								reconnectSpeakerWindow( event.source ); +							} +						} +					}); +				} + +				// Open the notes when the 's' key is hit +				deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() { +					openSpeakerWindow(); +				} ); + +			} + +		}, + +		open: openSpeakerWindow +	}; + +}; + +export default Plugin; | 
