|
13 | 13 | // limitations under the License.
|
14 | 14 |
|
15 | 15 | (function () {
|
16 |
| - let notes = document.querySelector("details"); |
17 |
| - // Create an unattached DOM node for the code below. |
18 |
| - if (!notes) { |
19 |
| - notes = document.createElement("details"); |
| 16 | + // Valid speaker notes states |
| 17 | + const NotesState = { |
| 18 | + Popup: "popup", |
| 19 | + Inline: "inline-open", |
| 20 | + Closed: "inline-closed", |
| 21 | + }; |
| 22 | + |
| 23 | + // The mode/function of this window |
| 24 | + const WindowMode = { |
| 25 | + Regular: "regular", |
| 26 | + RegularWithSpeakerNotes: "regular-speaker-notes", |
| 27 | + SpeakerNotes: "speaker-notes", |
| 28 | + PrintPage: "print-page", |
| 29 | + }; |
| 30 | + |
| 31 | + // detect the current window mode based on window location properties |
| 32 | + function detectWindowMode() { |
| 33 | + if (window.location.hash == "#speaker-notes-open") { |
| 34 | + return WindowMode.SpeakerNotes; |
| 35 | + } else if (window.location.hash == "#speaker-notes") { |
| 36 | + return WindowMode.RegularWithSpeakerNotes; |
| 37 | + } else if (window.location.pathname.endsWith("/print.html")) { |
| 38 | + return WindowMode.PrintPage; |
| 39 | + } else { |
| 40 | + return WindowMode.Regular; |
| 41 | + } |
20 | 42 | }
|
21 |
| - let popIn = document.createElement("button"); |
22 | 43 |
|
23 |
| - // Mark the speaker note window defunct. This means that it will no longer |
24 |
| - // show the notes. |
25 |
| - function markDefunct() { |
26 |
| - const main = document.querySelector("main"); |
27 |
| - const h4 = document.createElement("h4"); |
28 |
| - h4.append("(You can close this window now.)"); |
29 |
| - main.replaceChildren(h4); |
30 |
| - window.location.hash = "#speaker-notes-defunct"; |
| 44 | + // This channel is used to detect if a speaker notes window is open |
| 45 | + // The slides regularly pings the speaker notes window and the speaker notes send a pong |
| 46 | + // If that pong is missing, assume that the notes are closed |
| 47 | + const speakerNotesChannel = new BroadcastChannel("speaker-notes"); |
| 48 | + // Track if a pong was received |
| 49 | + var speakerNotesPongReceived = false; |
| 50 | + |
| 51 | + // Messages sent across the broadcast channel |
| 52 | + const BroadcastMessage = { |
| 53 | + Ping: "ping", |
| 54 | + Pong: "pong", |
| 55 | + CloseNotes: "close-notes", |
| 56 | + }; |
| 57 | + |
| 58 | + // Detect the speaker notes from the regular window |
| 59 | + function speakerNotesDetection() { |
| 60 | + // Reset the tracking variable |
| 61 | + speakerNotesPongReceived = false; |
| 62 | + // Send the ping |
| 63 | + speakerNotesChannel.postMessage(BroadcastMessage.Ping); |
| 64 | + setTimeout(() => { |
| 65 | + // Check if a pong message was received after the timeout of 500ms |
| 66 | + if (!speakerNotesPongReceived) { |
| 67 | + if (getSpeakerNotesState() == NotesState.Popup) { |
| 68 | + // Reset to Inline if we have been in Popup mode |
| 69 | + setSpeakerNotesState(NotesState.Inline); |
| 70 | + } |
| 71 | + } else { |
| 72 | + // Received a pong from a speaker notes window |
| 73 | + if (getSpeakerNotesState() != NotesState.Popup) { |
| 74 | + // but we are not in Popup mode, reset to Popup mode |
| 75 | + setSpeakerNotesState(NotesState.Popup); |
| 76 | + } |
| 77 | + } |
| 78 | + }, 500); |
31 | 79 | }
|
32 | 80 |
|
33 |
| - // Update the window. This shows/hides controls as necessary for regular and |
34 |
| - // speaker note pages. |
35 |
| - function applyState() { |
36 |
| - if (window.location.hash == "#speaker-notes-open") { |
37 |
| - if (getState() != "popup") { |
38 |
| - markDefunct(); |
| 81 | + // Handle broadcast messages |
| 82 | + speakerNotesChannel.onmessage = (event) => { |
| 83 | + if (detectWindowMode() == WindowMode.SpeakerNotes) { |
| 84 | + // Messages for the speaker notes window |
| 85 | + if (event.data == BroadcastMessage.Ping) { |
| 86 | + // Regular window sent a ping request, send answer |
| 87 | + speakerNotesChannel.postMessage(BroadcastMessage.Pong); |
| 88 | + } else if (event.data == BroadcastMessage.CloseNotes) { |
| 89 | + // Regular window sent a close request, close the window |
| 90 | + window.close(); |
| 91 | + } |
| 92 | + } else { |
| 93 | + // Messages for a regular window |
| 94 | + if (event.data == BroadcastMessage.Pong) { |
| 95 | + // Signal to the detection method that we received a pong |
| 96 | + speakerNotesPongReceived = true; |
39 | 97 | }
|
40 |
| - return; |
41 | 98 | }
|
| 99 | + }; |
42 | 100 |
|
43 |
| - switch (getState()) { |
44 |
| - case "popup": |
| 101 | + let notes = document.querySelector("details"); |
| 102 | + // Create an unattached DOM node for the code below. |
| 103 | + if (!notes) { |
| 104 | + notes = document.createElement("details"); |
| 105 | + } |
| 106 | + let popIn = document.createElement("button"); |
| 107 | + |
| 108 | + // Apply the correct style for the inline speaker notes in the |
| 109 | + // regular window - do not use on speaker notes page |
| 110 | + function applyInlinePopupStyle() { |
| 111 | + switch (getSpeakerNotesState()) { |
| 112 | + case NotesState.Popup: |
45 | 113 | popIn.classList.remove("hidden");
|
46 | 114 | notes.classList.add("hidden");
|
47 | 115 | break;
|
48 |
| - case "inline-open": |
| 116 | + case NotesState.Inline: |
49 | 117 | popIn.classList.add("hidden");
|
50 | 118 | notes.open = true;
|
51 | 119 | notes.classList.remove("hidden");
|
52 | 120 | break;
|
53 |
| - case "inline-closed": |
| 121 | + case NotesState.Closed: |
54 | 122 | popIn.classList.add("hidden");
|
55 | 123 | notes.open = false;
|
56 | 124 | notes.classList.remove("hidden");
|
57 | 125 | break;
|
58 | 126 | }
|
59 | 127 | }
|
60 | 128 |
|
61 |
| - // Get the state of the speaker note window: "inline-open", "inline-closed", |
62 |
| - // or "popup". |
63 |
| - function getState() { |
64 |
| - return window.localStorage["speakerNotes"] || "inline-closed"; |
| 129 | + // Get the state of the speaker note window. |
| 130 | + function getSpeakerNotesState() { |
| 131 | + return window.localStorage["speakerNotes"] || NotesState.Closed; |
65 | 132 | }
|
66 | 133 |
|
67 |
| - // Set the state of the speaker note window. Call applyState as needed |
68 |
| - // afterwards. |
69 |
| - function setState(state) { |
| 134 | + // Set the state of the speaker note window. |
| 135 | + function setSpeakerNotesState(state) { |
| 136 | + if (window.localStorage["speakerNotes"] == state) { |
| 137 | + // no change |
| 138 | + return; |
| 139 | + } |
70 | 140 | window.localStorage["speakerNotes"] = state;
|
| 141 | + applyInlinePopupStyle(); |
71 | 142 | }
|
72 | 143 |
|
73 | 144 | // Create controls for a regular page.
|
74 | 145 | function setupRegularPage() {
|
| 146 | + // Set-up a detector for speaker notes windows that pings |
| 147 | + // potential speaker note windows every 1000ms |
| 148 | + setInterval(speakerNotesDetection, 1000); |
| 149 | + |
75 | 150 | // Create pop-in button.
|
76 | 151 | popIn.setAttribute("id", "speaker-notes-toggle");
|
77 | 152 | popIn.setAttribute("type", "button");
|
|
82 | 157 | popInIcon.classList.add("fa", "fa-window-close-o");
|
83 | 158 | popIn.append(popInIcon);
|
84 | 159 | popIn.addEventListener("click", (event) => {
|
85 |
| - setState("inline-open"); |
86 |
| - applyState(); |
| 160 | + // Send a message to the speaker notes to close itself |
| 161 | + speakerNotesChannel.postMessage(BroadcastMessage.CloseNotes); |
| 162 | + // Switch to Inline popup mode |
| 163 | + setSpeakerNotesState(NotesState.Inline); |
87 | 164 | });
|
88 | 165 | document.querySelector(".left-buttons").append(popIn);
|
89 | 166 |
|
90 | 167 | // Create speaker notes.
|
91 | 168 | notes.addEventListener("toggle", (event) => {
|
92 |
| - setState(notes.open ? "inline-open" : "inline-closed"); |
| 169 | + // This always fires on first load on a regular page when applyInlinePopupStyle() |
| 170 | + // is called notes are opened (if NotesState.Inline) |
| 171 | + setSpeakerNotesState(notes.open ? NotesState.Inline : NotesState.Closed); |
93 | 172 | });
|
94 | 173 |
|
95 | 174 | let summary = document.createElement("summary");
|
|
111 | 190 | let popOut = document.createElement("button");
|
112 | 191 | popOut.classList.add("icon-button", "pop-out");
|
113 | 192 | popOut.addEventListener("click", (event) => {
|
114 |
| - let popup = window.open(popOutLocation.href, "speakerNotes", "popup"); |
| 193 | + let popup = window.open( |
| 194 | + popOutLocation.href, |
| 195 | + "speakerNotes", |
| 196 | + NotesState.Popup, |
| 197 | + ); |
115 | 198 | if (popup) {
|
116 |
| - setState("popup"); |
117 |
| - applyState(); |
118 |
| - // bind the popup to reset the speaker note state on close of the popup |
119 |
| - popup.onload = () => { |
120 |
| - popup.onbeforeunload = () => { |
121 |
| - setState("inline-open"); |
122 |
| - applyState(); |
123 |
| - }; |
124 |
| - }; |
| 199 | + setSpeakerNotesState(NotesState.Popup); |
125 | 200 | } else {
|
126 | 201 | window.alert(
|
127 | 202 | "Could not open popup, please check your popup blocker settings.",
|
|
195 | 270 | });
|
196 | 271 | }
|
197 | 272 |
|
198 |
| - let timeout = null; |
199 | 273 | // This will fire on _other_ open windows when we change window.localStorage.
|
200 | 274 | window.addEventListener("storage", (event) => {
|
201 | 275 | switch (event.key) {
|
202 | 276 | case "currentPage":
|
203 |
| - if (getState() == "popup") { |
| 277 | + if (getSpeakerNotesState() == NotesState.Popup) { |
204 | 278 | // We link all windows when we are showing speaker notes.
|
205 | 279 | window.location.pathname = event.newValue;
|
206 | 280 | }
|
207 | 281 | break;
|
208 |
| - case "speakerNotes": |
209 |
| - // When navigating to another page, we see two state changes in rapid |
210 |
| - // succession: |
211 |
| - // |
212 |
| - // - "popup" -> "inline-open" |
213 |
| - // - "inline-open" -> "popup" |
214 |
| - // |
215 |
| - // When the page is closed, we only see: |
216 |
| - // |
217 |
| - // - "popup" -> "inline-open" |
218 |
| - // |
219 |
| - // We can use a timeout to detect the difference. The effect is that |
220 |
| - // showing the speaker notes is delayed by 500 ms when closing the |
221 |
| - // speaker notes window. |
222 |
| - if (timeout) { |
223 |
| - clearTimeout(timeout); |
224 |
| - } |
225 |
| - timeout = setTimeout(applyState, 500); |
226 |
| - break; |
227 | 282 | }
|
228 | 283 | });
|
229 | 284 | window.localStorage["currentPage"] = window.location.pathname;
|
230 | 285 |
|
231 |
| - // We encode the kind of page in the location hash: |
232 |
| - switch (window.location.hash) { |
233 |
| - case "#speaker-notes-open": |
234 |
| - // We are on a page in the speaker notes. |
| 286 | + // apply the correct state for the window |
| 287 | + switch (detectWindowMode()) { |
| 288 | + case WindowMode.SpeakerNotes: |
235 | 289 | setupSpeakerNotes();
|
236 | 290 | break;
|
237 |
| - case "#speaker-notes-defunct": |
238 |
| - // We are on a page in a defunct speaker note window. We keep the state |
239 |
| - // unchanged and mark the window defunct. |
240 |
| - setupSpeakerNotes(); |
241 |
| - markDefunct(); |
| 291 | + case WindowMode.PrintPage: |
| 292 | + setupPrintPage(); |
242 | 293 | break;
|
243 |
| - default: |
244 |
| - if (window.location.pathname.endsWith("/print.html")) { |
245 |
| - setupPrintPage(); |
246 |
| - return; |
247 |
| - } |
248 |
| - |
249 |
| - // We are on a regular page. We force the state to "inline-open" if this |
250 |
| - // looks like a direct link to the speaker notes. |
251 |
| - if (window.location.hash == "#speaker-notes") { |
252 |
| - setState("inline-open"); |
253 |
| - } |
254 |
| - applyState(); |
| 294 | + case WindowMode.RegularWithSpeakerNotes: |
| 295 | + // Regular page with inline speaker notes, set state then fall-through |
| 296 | + setSpeakerNotesState(NotesState.Inline); |
| 297 | + case WindowMode.Regular: |
| 298 | + // Manually apply the style once |
| 299 | + applyInlinePopupStyle(); |
255 | 300 | setupRegularPage();
|
| 301 | + break; |
256 | 302 | }
|
257 | 303 | })();
|
0 commit comments