-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathspeaker.js
232 lines (202 loc) · 7.43 KB
/
speaker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
(() => {
var notesValue = document.querySelector('.speaker-controls-notes .value');
var slideCurrentNumber = document.querySelector('.slide-current-number');
var currentState;
var currentSlide;
var upcomingSlide;
var pendingCalls = {};
var lastRevealApiCallId = 0;
var connected = false;
callRevealApi('getTotalSlides', [], (total) => {
document.querySelector('.slide-total-number').textContent = total;
});
callRevealApi('getIndices', [], ({ h }) => {
document.querySelector('.slide-current-number').textContent = h;
});
document.querySelector('#prev').addEventListener('click', () => {
callRevealApi('prev');
});
document.querySelector('#next').addEventListener('click', () => {
callRevealApi('next');
});
window.opener.addEventListener('beforeunload', () => {
window.close();
});
window.addEventListener('message', function (event) {
var data = JSON.parse(event.data);
// The overview mode is only useful to the reveal.js instance
// where navigation occurs so we don't sync it
if (data.state) delete data.state.overview;
// Messages sent by the notes plugin inside of the main window
if (data && data.namespace === 'reveal-notes') {
if (data.type === 'connect') {
handleConnectMessage(data);
} else if (data.type === 'state') {
handleStateMessage(data);
} else if (data.type === 'return') {
pendingCalls[data.callId](data.result);
// delete pendingCalls[data.callId];
}
// Messages sent by the reveal.js inside of the current slide preview
} else if (data && data.namespace === 'reveal') {
if (/ready/.test(data.eventName)) {
// Send a message back to notify that the handshake is complete
window.opener.postMessage(JSON.stringify({ namespace: 'reveal-notes', type: 'connected' }), '*');
} else if (/slidechanged|fragmentshown|fragmenthidden|paused|resumed/.test(data.eventName) && currentState !== JSON.stringify(data.state)) {
window.opener.postMessage(JSON.stringify({ method: 'setState', args: [data.state] }), '*');
}
}
});
/**
* Asynchronously calls the Reveal.js API of the main frame.
*/
function callRevealApi (methodName, methodArguments, callback) {
var callId = ++lastRevealApiCallId;
pendingCalls[callId] = callback;
window.opener.postMessage(JSON.stringify({
namespace: 'reveal-notes',
type: 'call',
callId: callId,
methodName: methodName,
arguments: methodArguments
}), '*');
}
/**
* Called when the main window is trying to establish a
* connection.
*/
function handleConnectMessage (data) {
if (connected === false) {
connected = true;
setupIframes(data);
setupKeyboard();
setupTimer();
}
}
/**
* Called when the main window sends an updated state.
*/
const handleStateMessage = debounce((data) => {
// Store the most recently set state to avoid circular loops
// applying the same state
currentState = JSON.stringify(data.state);
slideCurrentNumber.textContent = data.state.indexh + 1;
// No need for updating the notes in case of fragment changes
if (data.notes) {
notesValue.style.whiteSpace = 'pre-wrap';
notesValue.innerHTML = data.notes;
} else {
notesValue.innerHTML = '';
}
// Update the note slides
currentSlide.contentWindow.postMessage(JSON.stringify({ method: 'setState', args: [data.state] }), '*');
upcomingSlide.contentWindow.postMessage(JSON.stringify({ method: 'setState', args: [data.state] }), '*');
upcomingSlide.contentWindow.postMessage(JSON.stringify({ method: 'next' }), '*');
}, 200);
/**
* Forward keyboard events to the current slide window.
* This enables keyboard events to work even if focus
* isn't set on the current slide iframe.
*
* Block F5 default handling, it reloads and disconnects
* the speaker notes window.
*/
function setupKeyboard () {
document.addEventListener('keydown', function (event) {
if (event.keyCode === 116 || (event.metaKey && event.keyCode === 82)) {
event.preventDefault();
return false;
}
currentSlide.contentWindow.postMessage(JSON.stringify({ method: 'triggerKey', args: [event.keyCode] }), '*');
});
}
function setupIframes (data) {
var params = [
'receiver',
'progress=false',
'history=false',
'transition=none',
'autoSlide=0',
'backgroundTransition=none'
].join('&');
var urlSeparator = /\?/.test(data.url) ? '&' : '?';
var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
var currentURL = data.url + urlSeparator + params + '&postMessageEvents=true' + hash;
var upcomingURL = data.url + urlSeparator + params + '&controls=false' + hash;
currentSlide = document.createElement('iframe');
currentSlide.setAttribute('width', 1280);
currentSlide.setAttribute('height', 1024);
currentSlide.setAttribute('src', currentURL);
document.querySelector('#current-slide').appendChild(currentSlide);
upcomingSlide = document.createElement('iframe');
upcomingSlide.setAttribute('width', 1280);
upcomingSlide.setAttribute('height', 1024);
upcomingSlide.setAttribute('src', upcomingURL);
document.querySelector('#upcoming-slide').appendChild(upcomingSlide);
}
function setupTimer () {
var start = new Date();
var timeEl = document.querySelector('.speaker-controls-time');
var clockEl = timeEl.querySelector('.clock-value');
var hoursEl = timeEl.querySelector('.hours-value');
var minutesEl = timeEl.querySelector('.minutes-value');
var secondsEl = timeEl.querySelector('.seconds-value');
_updateTimer();
setInterval(_updateTimer, 1000);
timeEl.addEventListener('click', function () {
start = new Date();
_updateTimer();
return false;
});
function _displayTime (hrEl, minEl, secEl, time) {
var sign = Math.sign(time) === -1 ? '-' : '';
time = Math.abs(Math.round(time / 1000));
var seconds = time % 60;
var minutes = Math.floor(time / 60) % 60;
var hours = Math.floor(time / (60 * 60));
hrEl.innerHTML = sign + zeroPadInteger(hours);
if (hours === 0) {
hrEl.classList.add('mute');
} else {
hrEl.classList.remove('mute');
}
minEl.innerHTML = ':' + zeroPadInteger(minutes);
if (hours === 0 && minutes === 0) {
minEl.classList.add('mute');
} else {
minEl.classList.remove('mute');
}
secEl.innerHTML = ':' + zeroPadInteger(seconds);
}
function _updateTimer () {
var diff;
var now = new Date();
diff = now.getTime() - start.getTime();
clockEl.innerHTML = now.toLocaleTimeString('en-US', { hour12: true, hour: '2-digit', minute: '2-digit' });
_displayTime(hoursEl, minutesEl, secondsEl, diff);
}
}
function zeroPadInteger (num) {
var str = '00' + parseInt(num);
return str.substring(str.length - 2);
}
function debounce (fn, ms) {
var lastTime = 0;
var timeout;
return function () {
var args = arguments;
var context = this;
clearTimeout(timeout);
var timeSinceLastCall = Date.now() - lastTime;
if (timeSinceLastCall > ms) {
fn.apply(context, args);
lastTime = Date.now();
} else {
timeout = setTimeout(function () {
fn.apply(context, args);
lastTime = Date.now();
}, ms - timeSinceLastCall);
}
};
}
})();