1/**
2 * Handles opening of and synchronization with the reveal.js
3 * notes window.
4 *
5 * Handshake process:
6 * 1. This window posts 'connect' to notes window
7 *    - Includes URL of presentation to show
8 * 2. Notes window responds with 'connected' when it is available
9 * 3. This window proceeds to send the current presentation state
10 *    to the notes window
11 */
12var RevealNotes = (function() {
13
14    var notesPopup = null;
15
16	function openNotes( notesFilePath ) {
17
18        if (notesPopup && !notesPopup.closed) {
19            notesPopup.focus();
20            return;
21        }
22
23		if( !notesFilePath ) {
24			var jsFileLocation = document.querySelector('script[src$="notes.js"]').src;  // this js file path
25			jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, '');   // the js folder path
26			notesFilePath = jsFileLocation + 'notes.html';
27		}
28
29		notesPopup = window.open( notesFilePath, 'reveal.js - Notes', 'width=1100,height=700' );
30
31		if( !notesPopup ) {
32			alert( 'Speaker view popup failed to open. Please make sure popups are allowed and reopen the speaker view.' );
33			return;
34		}
35
36		/**
37		 * Connect to the notes window through a postmessage handshake.
38		 * Using postmessage enables us to work in situations where the
39		 * origins differ, such as a presentation being opened from the
40		 * file system.
41		 */
42		function connect() {
43			// Keep trying to connect until we get a 'connected' message back
44			var connectInterval = setInterval( function() {
45				notesPopup.postMessage( JSON.stringify( {
46					namespace: 'reveal-notes',
47					type: 'connect',
48					url: window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search,
49					state: Reveal.getState()
50				} ), '*' );
51			}, 500 );
52
53			window.addEventListener( 'message', function( event ) {
54				var data = JSON.parse( event.data );
55				if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
56					clearInterval( connectInterval );
57					onConnected();
58				}
59				if( data && data.namespace === 'reveal-notes' && data.type === 'call' ) {
60					callRevealApi( data.methodName, data.arguments, data.callId );
61				}
62			} );
63		}
64
65		/**
66		 * Calls the specified Reveal.js method with the provided argument
67		 * and then pushes the result to the notes frame.
68		 */
69		function callRevealApi( methodName, methodArguments, callId ) {
70
71			var result = Reveal[methodName].apply( Reveal, methodArguments );
72			notesPopup.postMessage( JSON.stringify( {
73				namespace: 'reveal-notes',
74				type: 'return',
75				result: result,
76				callId: callId
77			} ), '*' );
78
79		}
80
81		/**
82		 * Posts the current slide data to the notes window
83		 */
84		function post( event ) {
85
86			var slideElement = Reveal.getCurrentSlide(),
87				notesElement = slideElement.querySelector( 'aside.notes' ),
88				fragmentElement = slideElement.querySelector( '.current-fragment' );
89
90			var messageData = {
91				namespace: 'reveal-notes',
92				type: 'state',
93				notes: '',
94				markdown: false,
95				whitespace: 'normal',
96				state: Reveal.getState()
97			};
98
99			// Look for notes defined in a slide attribute
100			if( slideElement.hasAttribute( 'data-notes' ) ) {
101				messageData.notes = slideElement.getAttribute( 'data-notes' );
102				messageData.whitespace = 'pre-wrap';
103			}
104
105			// Look for notes defined in a fragment
106			if( fragmentElement ) {
107				var fragmentNotes = fragmentElement.querySelector( 'aside.notes' );
108				if( fragmentNotes ) {
109					notesElement = fragmentNotes;
110				}
111				else if( fragmentElement.hasAttribute( 'data-notes' ) ) {
112					messageData.notes = fragmentElement.getAttribute( 'data-notes' );
113					messageData.whitespace = 'pre-wrap';
114
115					// In case there are slide notes
116					notesElement = null;
117				}
118			}
119
120			// Look for notes defined in an aside element
121			if( notesElement ) {
122				messageData.notes = notesElement.innerHTML;
123				messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string';
124			}
125
126			notesPopup.postMessage( JSON.stringify( messageData ), '*' );
127
128		}
129
130		/**
131		 * Called once we have established a connection to the notes
132		 * window.
133		 */
134		function onConnected() {
135
136			// Monitor events that trigger a change in state
137			Reveal.addEventListener( 'slidechanged', post );
138			Reveal.addEventListener( 'fragmentshown', post );
139			Reveal.addEventListener( 'fragmenthidden', post );
140			Reveal.addEventListener( 'overviewhidden', post );
141			Reveal.addEventListener( 'overviewshown', post );
142			Reveal.addEventListener( 'paused', post );
143			Reveal.addEventListener( 'resumed', post );
144
145			// Post the initial state
146			post();
147
148		}
149
150		connect();
151
152	}
153
154	return {
155		init: function() {
156
157			if( !/receiver/i.test( window.location.search ) ) {
158
159				// If the there's a 'notes' query set, open directly
160				if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) {
161					openNotes();
162				}
163
164				// Open the notes when the 's' key is hit
165				Reveal.addKeyBinding({keyCode: 83, key: 'S', description: 'Speaker notes view'}, function() {
166					openNotes();
167				} );
168
169			}
170
171		},
172
173		open: openNotes
174	};
175
176})();
177
178Reveal.registerPlugin( 'notes', RevealNotes );
179