1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-04-24 00:30:29 +02:00
comprehensive-rust/theme/speaker-notes.js
michael-kerscher f22395d278
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.
2025-02-28 12:06:06 -05:00

304 lines
10 KiB
JavaScript

// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function () {
// Valid speaker notes states
const NotesState = {
Popup: "popup",
Inline: "inline-open",
Closed: "inline-closed",
};
// The mode/function of this window
const WindowMode = {
Regular: "regular",
RegularWithSpeakerNotes: "regular-speaker-notes",
SpeakerNotes: "speaker-notes",
PrintPage: "print-page",
};
// detect the current window mode based on window location properties
function detectWindowMode() {
if (window.location.hash == "#speaker-notes-open") {
return WindowMode.SpeakerNotes;
} else if (window.location.hash == "#speaker-notes") {
return WindowMode.RegularWithSpeakerNotes;
} else if (window.location.pathname.endsWith("/print.html")) {
return WindowMode.PrintPage;
} else {
return WindowMode.Regular;
}
}
// This channel is used to detect if a speaker notes window is open
// The slides regularly pings the speaker notes window and the speaker notes send a pong
// If that pong is missing, assume that the notes are closed
const speakerNotesChannel = new BroadcastChannel("speaker-notes");
// Track if a pong was received
var speakerNotesPongReceived = false;
// Messages sent across the broadcast channel
const BroadcastMessage = {
Ping: "ping",
Pong: "pong",
CloseNotes: "close-notes",
};
// Detect the speaker notes from the regular window
function speakerNotesDetection() {
// Reset the tracking variable
speakerNotesPongReceived = false;
// Send the ping
speakerNotesChannel.postMessage(BroadcastMessage.Ping);
setTimeout(() => {
// Check if a pong message was received after the timeout of 500ms
if (!speakerNotesPongReceived) {
if (getSpeakerNotesState() == NotesState.Popup) {
// Reset to Inline if we have been in Popup mode
setSpeakerNotesState(NotesState.Inline);
}
} else {
// Received a pong from a speaker notes window
if (getSpeakerNotesState() != NotesState.Popup) {
// but we are not in Popup mode, reset to Popup mode
setSpeakerNotesState(NotesState.Popup);
}
}
}, 500);
}
// Handle broadcast messages
speakerNotesChannel.onmessage = (event) => {
if (detectWindowMode() == WindowMode.SpeakerNotes) {
// Messages for the speaker notes window
if (event.data == BroadcastMessage.Ping) {
// Regular window sent a ping request, send answer
speakerNotesChannel.postMessage(BroadcastMessage.Pong);
} else if (event.data == BroadcastMessage.CloseNotes) {
// Regular window sent a close request, close the window
window.close();
}
} else {
// Messages for a regular window
if (event.data == BroadcastMessage.Pong) {
// Signal to the detection method that we received a pong
speakerNotesPongReceived = true;
}
}
};
let notes = document.querySelector("details");
// Create an unattached DOM node for the code below.
if (!notes) {
notes = document.createElement("details");
}
let popIn = document.createElement("button");
// Apply the correct style for the inline speaker notes in the
// regular window - do not use on speaker notes page
function applyInlinePopupStyle() {
switch (getSpeakerNotesState()) {
case NotesState.Popup:
popIn.classList.remove("hidden");
notes.classList.add("hidden");
break;
case NotesState.Inline:
popIn.classList.add("hidden");
notes.open = true;
notes.classList.remove("hidden");
break;
case NotesState.Closed:
popIn.classList.add("hidden");
notes.open = false;
notes.classList.remove("hidden");
break;
}
}
// Get the state of the speaker note window.
function getSpeakerNotesState() {
return window.localStorage["speakerNotes"] || NotesState.Closed;
}
// Set the state of the speaker note window.
function setSpeakerNotesState(state) {
if (window.localStorage["speakerNotes"] == state) {
// no change
return;
}
window.localStorage["speakerNotes"] = state;
applyInlinePopupStyle();
}
// Create controls for a regular page.
function setupRegularPage() {
// Set-up a detector for speaker notes windows that pings
// potential speaker note windows every 1000ms
setInterval(speakerNotesDetection, 1000);
// Create pop-in button.
popIn.setAttribute("id", "speaker-notes-toggle");
popIn.setAttribute("type", "button");
popIn.setAttribute("title", "Close speaker notes");
popIn.setAttribute("aria-label", "Close speaker notes");
popIn.classList.add("icon-button");
let popInIcon = document.createElement("i");
popInIcon.classList.add("fa", "fa-window-close-o");
popIn.append(popInIcon);
popIn.addEventListener("click", (event) => {
// Send a message to the speaker notes to close itself
speakerNotesChannel.postMessage(BroadcastMessage.CloseNotes);
// Switch to Inline popup mode
setSpeakerNotesState(NotesState.Inline);
});
document.querySelector(".left-buttons").append(popIn);
// Create speaker notes.
notes.addEventListener("toggle", (event) => {
// This always fires on first load on a regular page when applyInlinePopupStyle()
// is called notes are opened (if NotesState.Inline)
setSpeakerNotesState(notes.open ? NotesState.Inline : NotesState.Closed);
});
let summary = document.createElement("summary");
notes.insertBefore(summary, notes.firstChild);
let h4 = document.createElement("h4");
h4.setAttribute("id", "speaker-notes");
h4.append("Speaker Notes");
h4.addEventListener("click", (event) => {
// Update fragment as if we had clicked a link. A regular a element would
// result in double-fire of the event.
window.location.hash = "#speaker-notes";
});
summary.append(h4);
// Create pop-out button.
let popOutLocation = new URL(window.location.href);
popOutLocation.hash = "#speaker-notes-open";
let popOut = document.createElement("button");
popOut.classList.add("icon-button", "pop-out");
popOut.addEventListener("click", (event) => {
let popup = window.open(
popOutLocation.href,
"speakerNotes",
NotesState.Popup,
);
if (popup) {
setSpeakerNotesState(NotesState.Popup);
} else {
window.alert(
"Could not open popup, please check your popup blocker settings.",
);
}
});
let popOutIcon = document.createElement("i");
popOutIcon.classList.add("fa", "fa-external-link");
popOut.append(popOutIcon);
summary.append(popOut);
}
// Create headers on the print page.
function setupPrintPage() {
for (const notes of document.querySelectorAll("details")) {
notes.open = true;
let summary = document.createElement("summary");
notes.insertBefore(summary, notes.firstChild);
let h4 = document.createElement("h4");
h4.append("Speaker Notes");
summary.append(h4);
}
}
// Create controls for a speaker note window.
function setupSpeakerNotes() {
// Hide sidebar and buttons.
document.querySelector("html").classList.remove("sidebar-visible");
document.querySelector("html").classList.add("sidebar-hidden");
document.querySelector(".left-buttons").classList.add("hidden");
document.querySelector(".right-buttons").classList.add("hidden");
// Hide content except for the speaker notes and h1 elements.
const main = document.querySelector("main");
let children = main.childNodes;
let i = 0;
while (i < children.length) {
const node = children[i];
switch (node.tagName) {
case "DETAILS":
// We found the speaker notes: extract their content.
let div = document.createElement("div");
div.replaceChildren(...node.childNodes);
node.replaceWith(div);
i += 1;
break;
case "H1":
// We found a header: turn it into a smaller header for the speaker
// note window.
let h4 = document.createElement("h4");
let pageLocation = new URL(window.location.href);
pageLocation.hash = "";
let a = document.createElement("a");
a.setAttribute("href", pageLocation.href);
a.append(node.innerText);
h4.append("Speaker Notes for ", a);
node.replaceWith(h4);
i += 1;
break;
default:
// We found something else: remove it.
main.removeChild(node);
}
}
// Update prev/next buttons to keep speaker note state.
document
.querySelectorAll('a[rel~="prev"], a[rel~="next"]')
.forEach((elem) => {
elem.href += "#speaker-notes-open";
});
}
// This will fire on _other_ open windows when we change window.localStorage.
window.addEventListener("storage", (event) => {
switch (event.key) {
case "currentPage":
if (getSpeakerNotesState() == NotesState.Popup) {
// We link all windows when we are showing speaker notes.
window.location.pathname = event.newValue;
}
break;
}
});
window.localStorage["currentPage"] = window.location.pathname;
// apply the correct state for the window
switch (detectWindowMode()) {
case WindowMode.SpeakerNotes:
setupSpeakerNotes();
break;
case WindowMode.PrintPage:
setupPrintPage();
break;
case WindowMode.RegularWithSpeakerNotes:
// Regular page with inline speaker notes, set state then fall-through
setSpeakerNotesState(NotesState.Inline);
case WindowMode.Regular:
// Manually apply the style once
applyInlinePopupStyle();
setupRegularPage();
break;
}
})();