2022-12-27 16:52:41 +01:00
|
|
|
// 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.
|
|
|
|
|
2023-10-02 07:48:00 +02:00
|
|
|
(function () {
|
2025-02-28 18:06:06 +01:00
|
|
|
// 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;
|
|
|
|
}
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
// 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);
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
// 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;
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
}
|
2025-02-28 18:06:06 +01:00
|
|
|
};
|
2022-12-27 16:52:41 +01:00
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
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:
|
2022-12-27 16:52:41 +01:00
|
|
|
popIn.classList.remove("hidden");
|
|
|
|
notes.classList.add("hidden");
|
|
|
|
break;
|
2025-02-28 18:06:06 +01:00
|
|
|
case NotesState.Inline:
|
2022-12-27 16:52:41 +01:00
|
|
|
popIn.classList.add("hidden");
|
|
|
|
notes.open = true;
|
|
|
|
notes.classList.remove("hidden");
|
|
|
|
break;
|
2025-02-28 18:06:06 +01:00
|
|
|
case NotesState.Closed:
|
2022-12-27 16:52:41 +01:00
|
|
|
popIn.classList.add("hidden");
|
|
|
|
notes.open = false;
|
|
|
|
notes.classList.remove("hidden");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
// Get the state of the speaker note window.
|
|
|
|
function getSpeakerNotesState() {
|
|
|
|
return window.localStorage["speakerNotes"] || NotesState.Closed;
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
// Set the state of the speaker note window.
|
|
|
|
function setSpeakerNotesState(state) {
|
|
|
|
if (window.localStorage["speakerNotes"] == state) {
|
|
|
|
// no change
|
|
|
|
return;
|
|
|
|
}
|
2022-12-27 16:52:41 +01:00
|
|
|
window.localStorage["speakerNotes"] = state;
|
2025-02-28 18:06:06 +01:00
|
|
|
applyInlinePopupStyle();
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create controls for a regular page.
|
|
|
|
function setupRegularPage() {
|
2025-02-28 18:06:06 +01:00
|
|
|
// Set-up a detector for speaker notes windows that pings
|
|
|
|
// potential speaker note windows every 1000ms
|
|
|
|
setInterval(speakerNotesDetection, 1000);
|
|
|
|
|
2022-12-27 16:52:41 +01:00
|
|
|
// 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");
|
2023-01-12 12:26:07 +01:00
|
|
|
let popInIcon = document.createElement("i");
|
|
|
|
popInIcon.classList.add("fa", "fa-window-close-o");
|
|
|
|
popIn.append(popInIcon);
|
2022-12-27 16:52:41 +01:00
|
|
|
popIn.addEventListener("click", (event) => {
|
2025-02-28 18:06:06 +01:00
|
|
|
// Send a message to the speaker notes to close itself
|
|
|
|
speakerNotesChannel.postMessage(BroadcastMessage.CloseNotes);
|
|
|
|
// Switch to Inline popup mode
|
|
|
|
setSpeakerNotesState(NotesState.Inline);
|
2022-12-27 16:52:41 +01:00
|
|
|
});
|
|
|
|
document.querySelector(".left-buttons").append(popIn);
|
|
|
|
|
|
|
|
// Create speaker notes.
|
|
|
|
notes.addEventListener("toggle", (event) => {
|
2025-02-28 18:06:06 +01:00
|
|
|
// 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);
|
2022-12-27 16:52:41 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
let summary = document.createElement("summary");
|
|
|
|
notes.insertBefore(summary, notes.firstChild);
|
|
|
|
|
2025-01-16 22:53:58 -08:00
|
|
|
let h4 = document.createElement("h4");
|
|
|
|
h4.setAttribute("id", "speaker-notes");
|
|
|
|
h4.append("Speaker Notes");
|
|
|
|
h4.addEventListener("click", (event) => {
|
2022-12-27 16:52:41 +01:00
|
|
|
// 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";
|
|
|
|
});
|
2025-01-16 22:53:58 -08:00
|
|
|
summary.append(h4);
|
2022-12-27 16:52:41 +01:00
|
|
|
|
|
|
|
// Create pop-out button.
|
|
|
|
let popOutLocation = new URL(window.location.href);
|
|
|
|
popOutLocation.hash = "#speaker-notes-open";
|
2023-01-12 12:26:07 +01:00
|
|
|
let popOut = document.createElement("button");
|
|
|
|
popOut.classList.add("icon-button", "pop-out");
|
|
|
|
popOut.addEventListener("click", (event) => {
|
2025-02-28 18:06:06 +01:00
|
|
|
let popup = window.open(
|
|
|
|
popOutLocation.href,
|
|
|
|
"speakerNotes",
|
|
|
|
NotesState.Popup,
|
|
|
|
);
|
2023-01-12 12:26:07 +01:00
|
|
|
if (popup) {
|
2025-02-28 18:06:06 +01:00
|
|
|
setSpeakerNotesState(NotesState.Popup);
|
2023-01-12 12:26:07 +01:00
|
|
|
} else {
|
2023-10-02 07:48:00 +02:00
|
|
|
window.alert(
|
2025-02-28 15:27:17 +01:00
|
|
|
"Could not open popup, please check your popup blocker settings.",
|
2023-10-02 07:48:00 +02:00
|
|
|
);
|
2023-01-12 12:26:07 +01:00
|
|
|
}
|
2023-10-02 07:48:00 +02:00
|
|
|
});
|
2023-01-12 12:26:07 +01:00
|
|
|
let popOutIcon = document.createElement("i");
|
|
|
|
popOutIcon.classList.add("fa", "fa-external-link");
|
|
|
|
popOut.append(popOutIcon);
|
2022-12-27 16:52:41 +01:00
|
|
|
summary.append(popOut);
|
|
|
|
}
|
|
|
|
|
2023-01-06 11:39:33 +01:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-27 16:52:41 +01:00
|
|
|
// 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.
|
2023-10-02 07:48:00 +02:00
|
|
|
document
|
2025-02-08 11:47:02 +01:00
|
|
|
.querySelectorAll('a[rel~="prev"], a[rel~="next"]')
|
2023-10-02 07:48:00 +02:00
|
|
|
.forEach((elem) => {
|
|
|
|
elem.href += "#speaker-notes-open";
|
|
|
|
});
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// This will fire on _other_ open windows when we change window.localStorage.
|
|
|
|
window.addEventListener("storage", (event) => {
|
|
|
|
switch (event.key) {
|
|
|
|
case "currentPage":
|
2025-02-28 18:06:06 +01:00
|
|
|
if (getSpeakerNotesState() == NotesState.Popup) {
|
2022-12-27 16:52:41 +01:00
|
|
|
// We link all windows when we are showing speaker notes.
|
|
|
|
window.location.pathname = event.newValue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
window.localStorage["currentPage"] = window.location.pathname;
|
|
|
|
|
2025-02-28 18:06:06 +01:00
|
|
|
// apply the correct state for the window
|
|
|
|
switch (detectWindowMode()) {
|
|
|
|
case WindowMode.SpeakerNotes:
|
2022-12-27 16:52:41 +01:00
|
|
|
setupSpeakerNotes();
|
|
|
|
break;
|
2025-02-28 18:06:06 +01:00
|
|
|
case WindowMode.PrintPage:
|
|
|
|
setupPrintPage();
|
2022-12-27 16:52:41 +01:00
|
|
|
break;
|
2025-02-28 18:06:06 +01:00
|
|
|
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();
|
2022-12-27 16:52:41 +01:00
|
|
|
setupRegularPage();
|
2025-02-28 18:06:06 +01:00
|
|
|
break;
|
2022-12-27 16:52:41 +01:00
|
|
|
}
|
|
|
|
})();
|