Path: blob/main/src/resources/formats/revealjs/reveal/plugin/notes/plugin.js
12923 views
import speakerViewHTML from './speaker-view.html'12import { marked } from 'marked';34/**5* Handles opening of and synchronization with the reveal.js6* notes window.7*8* Handshake process:9* 1. This window posts 'connect' to notes window10* - Includes URL of presentation to show11* 2. Notes window responds with 'connected' when it is available12* 3. This window proceeds to send the current presentation state13* to the notes window14*/15const Plugin = () => {1617let connectInterval;18let speakerWindow = null;19let deck;2021/**22* Opens a new speaker view window.23*/24function openSpeakerWindow() {2526// If a window is already open, focus it27if( speakerWindow && !speakerWindow.closed ) {28speakerWindow.focus();29}30else {31speakerWindow = window.open( 'about:blank', 'reveal.js - Notes', 'width=1100,height=700' );32speakerWindow.marked = marked;33speakerWindow.document.write( speakerViewHTML );3435if( !speakerWindow ) {36alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );37return;38}3940connect();41}4243}4445/**46* Reconnect with an existing speaker view window.47*/48function reconnectSpeakerWindow( reconnectWindow ) {4950if( speakerWindow && !speakerWindow.closed ) {51speakerWindow.focus();52}53else {54speakerWindow = reconnectWindow;55window.addEventListener( 'message', onPostMessage );56onConnected();57}5859}6061/**62* Connect to the notes window through a postmessage handshake.63* Using postmessage enables us to work in situations where the64* origins differ, such as a presentation being opened from the65* file system.66*/67function connect() {6869const presentationURL = deck.getConfig().url;7071const url = typeof presentationURL === 'string' ? presentationURL :72window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search;7374// Keep trying to connect until we get a 'connected' message back75connectInterval = setInterval( function() {76speakerWindow.postMessage( JSON.stringify( {77namespace: 'reveal-notes',78type: 'connect',79state: deck.getState(),80url81} ), '*' );82}, 500 );8384window.addEventListener( 'message', onPostMessage );8586}8788/**89* Calls the specified Reveal.js method with the provided argument90* and then pushes the result to the notes frame.91*/92function callRevealApi( methodName, methodArguments, callId ) {9394let result = deck[methodName].apply( deck, methodArguments );95speakerWindow.postMessage( JSON.stringify( {96namespace: 'reveal-notes',97type: 'return',98result,99callId100} ), '*' );101102}103104/**105* Posts the current slide data to the notes window.106*/107function post( event ) {108109let slideElement = deck.getCurrentSlide(),110notesElements = slideElement.querySelectorAll( 'aside.notes' ),111fragmentElement = slideElement.querySelector( '.current-fragment' );112113let messageData = {114namespace: 'reveal-notes',115type: 'state',116notes: '',117markdown: false,118whitespace: 'normal',119state: deck.getState()120};121122// Look for notes defined in a slide attribute123if( slideElement.hasAttribute( 'data-notes' ) ) {124messageData.notes = slideElement.getAttribute( 'data-notes' );125messageData.whitespace = 'pre-wrap';126}127128// Look for notes defined in a fragment129if( fragmentElement ) {130let fragmentNotes = fragmentElement.querySelector( 'aside.notes' );131if( fragmentNotes ) {132messageData.notes = fragmentNotes.innerHTML;133messageData.markdown = typeof fragmentNotes.getAttribute( 'data-markdown' ) === 'string';134135// Ignore other slide notes136notesElements = null;137}138else if( fragmentElement.hasAttribute( 'data-notes' ) ) {139messageData.notes = fragmentElement.getAttribute( 'data-notes' );140messageData.whitespace = 'pre-wrap';141142// In case there are slide notes143notesElements = null;144}145}146147// Look for notes defined in an aside element148if( notesElements && notesElements.length ) {149// Ignore notes inside of fragments since those are shown150// individually when stepping through fragments151notesElements = Array.from( notesElements ).filter( notesElement => notesElement.closest( '.fragment' ) === null );152153messageData.notes = notesElements.map( notesElement => notesElement.innerHTML ).join( '\n' );154messageData.markdown = notesElements[0] && typeof notesElements[0].getAttribute( 'data-markdown' ) === 'string';155}156157speakerWindow.postMessage( JSON.stringify( messageData ), '*' );158159}160161/**162* Check if the given event is from the same origin as the163* current window.164*/165function isSameOriginEvent( event ) {166167try {168return window.location.origin === event.source.location.origin;169}170catch ( error ) {171return false;172}173174}175176function onPostMessage( event ) {177178// Only allow same-origin messages179// (added 12/5/22 as a XSS safeguard)180if( isSameOriginEvent( event ) ) {181182try {183let data = JSON.parse( event.data );184if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {185clearInterval( connectInterval );186onConnected();187}188else if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {189callRevealApi( data.methodName, data.arguments, data.callId );190}191} catch (e) {}192193}194195}196197/**198* Called once we have established a connection to the notes199* window.200*/201function onConnected() {202203// Monitor events that trigger a change in state204deck.on( 'slidechanged', post );205deck.on( 'fragmentshown', post );206deck.on( 'fragmenthidden', post );207deck.on( 'overviewhidden', post );208deck.on( 'overviewshown', post );209deck.on( 'paused', post );210deck.on( 'resumed', post );211212// Post the initial state213post();214215}216217return {218id: 'notes',219220init: function( reveal ) {221222deck = reveal;223224if( !/receiver/i.test( window.location.search ) ) {225226// If the there's a 'notes' query set, open directly227if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {228openSpeakerWindow();229}230else {231// Keep listening for speaker view hearbeats. If we receive a232// heartbeat from an orphaned window, reconnect it. This ensures233// that we remain connected to the notes even if the presentation234// is reloaded.235window.addEventListener( 'message', event => {236237if( !speakerWindow && typeof event.data === 'string' ) {238let data;239240try {241data = JSON.parse( event.data );242}243catch( error ) {}244245if( data && data.namespace === 'reveal-notes' && data.type === 'heartbeat' ) {246reconnectSpeakerWindow( event.source );247}248}249});250}251252// Open the notes when the 's' key is hit253deck.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {254openSpeakerWindow();255} );256257}258259},260261open: openSpeakerWindow262};263264};265266export default Plugin;267268269