// 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; } })();