Skip to content

Commit f22395d

Browse files
Fix bug where speaker notes are not connected to regular window (#2675)
Fixes bug #2004. Refactored the communication between the speaker notes window and the regular window by using a Broadcast channel - this is now self-recovering(!) even if speaker notes are closed and manually re-opened! For better readability and maintainability refactored some string-based states into enum style code and refactored detection of the type of windows (print, speaker note, regular window) Manually tested the new code and the speaker notes window does not disconnect from the regular window anymore. This now works way more reliable, even if there are (still) some UI glitches that have been there before already.
1 parent 0134568 commit f22395d

File tree

1 file changed

+132
-86
lines changed

1 file changed

+132
-86
lines changed

theme/speaker-notes.js

Lines changed: 132 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,140 @@
1313
// limitations under the License.
1414

1515
(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+
}
2042
}
21-
let popIn = document.createElement("button");
2243

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);
3179
}
3280

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;
3997
}
40-
return;
4198
}
99+
};
42100

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:
45113
popIn.classList.remove("hidden");
46114
notes.classList.add("hidden");
47115
break;
48-
case "inline-open":
116+
case NotesState.Inline:
49117
popIn.classList.add("hidden");
50118
notes.open = true;
51119
notes.classList.remove("hidden");
52120
break;
53-
case "inline-closed":
121+
case NotesState.Closed:
54122
popIn.classList.add("hidden");
55123
notes.open = false;
56124
notes.classList.remove("hidden");
57125
break;
58126
}
59127
}
60128

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;
65132
}
66133

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+
}
70140
window.localStorage["speakerNotes"] = state;
141+
applyInlinePopupStyle();
71142
}
72143

73144
// Create controls for a regular page.
74145
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+
75150
// Create pop-in button.
76151
popIn.setAttribute("id", "speaker-notes-toggle");
77152
popIn.setAttribute("type", "button");
@@ -82,14 +157,18 @@
82157
popInIcon.classList.add("fa", "fa-window-close-o");
83158
popIn.append(popInIcon);
84159
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);
87164
});
88165
document.querySelector(".left-buttons").append(popIn);
89166

90167
// Create speaker notes.
91168
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);
93172
});
94173

95174
let summary = document.createElement("summary");
@@ -111,17 +190,13 @@
111190
let popOut = document.createElement("button");
112191
popOut.classList.add("icon-button", "pop-out");
113192
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+
);
115198
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);
125200
} else {
126201
window.alert(
127202
"Could not open popup, please check your popup blocker settings.",
@@ -195,63 +270,34 @@
195270
});
196271
}
197272

198-
let timeout = null;
199273
// This will fire on _other_ open windows when we change window.localStorage.
200274
window.addEventListener("storage", (event) => {
201275
switch (event.key) {
202276
case "currentPage":
203-
if (getState() == "popup") {
277+
if (getSpeakerNotesState() == NotesState.Popup) {
204278
// We link all windows when we are showing speaker notes.
205279
window.location.pathname = event.newValue;
206280
}
207281
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;
227282
}
228283
});
229284
window.localStorage["currentPage"] = window.location.pathname;
230285

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:
235289
setupSpeakerNotes();
236290
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();
242293
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();
255300
setupRegularPage();
301+
break;
256302
}
257303
})();

0 commit comments

Comments
 (0)