bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

plugin.js (6936B)


      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;