diff --git a/presentations/ac2024/Templates/Lato-Bold.woff b/presentations/ac2024/Templates/Lato-Bold.woff new file mode 100644 index 0000000..4ade1ca Binary files /dev/null and b/presentations/ac2024/Templates/Lato-Bold.woff differ diff --git a/presentations/ac2024/Templates/Lato-BoldItalic.woff b/presentations/ac2024/Templates/Lato-BoldItalic.woff new file mode 100644 index 0000000..ed1f24b Binary files /dev/null and b/presentations/ac2024/Templates/Lato-BoldItalic.woff differ diff --git a/presentations/ac2024/Templates/Lato-Italic.woff b/presentations/ac2024/Templates/Lato-Italic.woff new file mode 100644 index 0000000..6e8210b Binary files /dev/null and b/presentations/ac2024/Templates/Lato-Italic.woff differ diff --git a/presentations/ac2024/Templates/Lato-Regular.woff b/presentations/ac2024/Templates/Lato-Regular.woff new file mode 100644 index 0000000..a1cb857 Binary files /dev/null and b/presentations/ac2024/Templates/Lato-Regular.woff differ diff --git a/presentations/ac2024/Templates/Montserrat-Black.woff b/presentations/ac2024/Templates/Montserrat-Black.woff new file mode 100644 index 0000000..0143ba1 Binary files /dev/null and b/presentations/ac2024/Templates/Montserrat-Black.woff differ diff --git a/presentations/ac2024/Templates/Montserrat-BlackItalic.woff b/presentations/ac2024/Templates/Montserrat-BlackItalic.woff new file mode 100644 index 0000000..d04ea2b Binary files /dev/null and b/presentations/ac2024/Templates/Montserrat-BlackItalic.woff differ diff --git a/presentations/ac2024/Templates/Montserrat-Bold.woff b/presentations/ac2024/Templates/Montserrat-Bold.woff new file mode 100644 index 0000000..5c96097 Binary files /dev/null and b/presentations/ac2024/Templates/Montserrat-Bold.woff differ diff --git a/presentations/ac2024/Templates/Montserrat-BoldItalic.woff b/presentations/ac2024/Templates/Montserrat-BoldItalic.woff new file mode 100644 index 0000000..2e87b5b Binary files /dev/null and b/presentations/ac2024/Templates/Montserrat-BoldItalic.woff differ diff --git a/presentations/ac2024/Templates/b6plus.js b/presentations/ac2024/Templates/b6plus.js new file mode 100644 index 0000000..66fca5e --- /dev/null +++ b/presentations/ac2024/Templates/b6plus.js @@ -0,0 +1,2323 @@ +/* b6plus.js $Revision: 1.95 $ + * + * Script to simulate projection mode on browsers that don't support + * media=projection or 'overflow-block: paged' (or ‘overflow-block: + * optional-paged’, from the 2014 Media Queries draft) but do support + * Javascript. + * + * Documentation and latest version: + * + * https://www.w3.org/Talks/Tools/b6plus/ + * + * Brief usage instructions: + * + * Add the script to a page with + * + * + * + * The script assumes each slide starts with an H1 or an element with + * class "slide", which is a direct child of the BODY. All elements + * until the next H1 or class "slide" are part of the slide, except + * for those with a class of "comment", which are hidden in slide + * mode. + * + * Elements with a class of "progress", "slidenum" or "numslides" are + * treated specially. They can be used to display progress in the + * slide show, as follows. Elements with a class of "numslides" will + * have their content replaced by the total number of slides in + * decimal. Elements with a class of "slidenum" will have their + * content replaced by the number of the currently displayed slide in + * decimal. The first slide is numbered 1. Elements with a class of + * "progress" will get a 'width' property whose value is a percentage + * between 0% and 100%, corresponding to the progress in the slide + * show: if there are M slide in total and the currently displayed + * slide is number N, the 'width' property will be N/M * 100%. + * + * There can be as many of these elements as desired. If they are + * defined as children of the BODY, they will be visible all the time. + * Otherwise their visibility depends on their parent. + * + * Usage: + * + * - Press A to toggle normal and slide mode. The script starts in + * normal mode. + * + * - Press Page-Down to go to the next slide. Press Page-Up, up arrow + * or left arrow to back-up one page. + * + * - Press Space, right arrow, down arrow or mouse button 1 to advance + * (incremental display or next slide) + * + * On touch screens, a tap with three fingers toggles slide mode, a + * wipe right goes back one slide, and wipe left advances. + * + * TODO: There is a proposal for HTML5 to allow fullscreen popup + * windows, maybe via Window.open(url, "window title", + * "popup,fullscreen") maybe via Element.Requestfullscreen(), and + * possibly with a way to select the screen to pop up on, if there are + * several. If that gets implemented, and we detect that there is + * another screen, we could present a button to open the slide show + * directly in fullscreen on the other screen. + * + * TODO: don't do anything if media = projection + * + * TODO: option to allow clicking in the left third of a slide to go + * back? + * + * TODO: Accessibility of the second window. + * + * TODO: Show an icon in the corner when sync mode is on? + * + * TODO: Allow a language for localized messages and clocks that is + * different from the slides' language? + * + * TODO: More or other syntaxes for commands in syncSlide()? "all" or + * "index" for "0"; "<", "previous" for "-"; ">", "next" for "+"; + * "last" for "$"... + * + * Originally derived from code by Dave Raggett. + * + * Author: Bert Bos + * Created: May 23, 2005 (b5) + * Modified: Jan 2012 (b5 -> b6) + * Modified: Oct 2016 (added jump to ID; fixes bugs with Home/End key handling) + * Modified: Apr 2018 (added touch events) + * Modified: May 2018 (support 'overflow-block' from Media Queries 4) + * Modified: Mar 2019 (support fixed aspect ratio, progress elements, b6 -> b6+) + * Modified: Aug 2020 (add class=visited to past elts in incremental display) + * Modified: Oct 2020 (start in slide mode if URL contains "?full") + * Modified: Apr 2021 (disable navigation if URL contains ‘?static’) + * Modified: May 2021 (rescale if window size changes while in slide mode) + * Modified: Jun 2021 (only one incremental item active, as in Shower since 3.1) + * Modified: Sep 2021 (a11y: added role=application and a live region) + * Modified: Dec 2021 (added noclick option; set slide number in URL if no ID) + * Modified: Dec 2021 (Added popup help tied to the "?" key) + * Modified: Apr 2022 (Added support for a second window, tied to the "2" key) + * Modified: Apr 2022 (forwarding of events in the second window to the first) + * Modified: Aug 2022 (help popup appears in the 2nd window if requested there) + * Modified: Nov 2022 (support server-sent events to sync slides) + * Modified: Nov 2022 (added clocks; localized to German, French and Dutch) + * Modified: Dec 2022 (protect against loading b6plus.js twice) + * Modified: Sep 2023 (show buttons in index mode to go to slide mode and more) + * Modified: Jan 2024 (swapped UI: 2nd window for slides, 1st for preview) + * + * Copyright 2005-2024 W3C, ERCIM + * See http://www.w3.org/Consortium/Legal/copyright-software + */ + +(function() { + +"use strict"; + +/* Localized strings */ +const translations = { + "min": { // Abbreviation for "minutes" + de: "Min", + fr: "min", + nl: "min"}, + "current time": { + de: "aktuelle Uhrzeit", + fr: "heure actuelle", + nl: "huidige tijd"}, + "used": { + de: "verbraucht", + fr: "utilisé", + nl: "gebruikt"}, + "remaining": { // As in "remaining time" + de: "Restzeit", + fr: "restant", + nl: "resterend"}, + "pause": { // Label on a button to pause the clock + de: "Pause", + fr: "pause", + nl: "pauze"}, + "resume": { // Label in a button to pause the clock + de: "fortsetzen", + fr: "reprendre", + nl: "hervatten"}, + "+1 min": { // Label on a button to add 1 minute + de: "+1 Min", + fr: "+1 min", + nl: "+1 min"}, + "−1 min": { // Label on a button to shorten time by 1 minute + de: "−1 Min", + fr: "−1 min", + nl: "−1 min"}, + "restart": { // Label on a button to reset the clock + de: "Neustart", + fr: "réinitialiser", + nl: "herstart"}, + "No navigation possible while sync mode is on.": { + de: "Bei aktiviertem Sync-Modus ist keine Navigation möglich.", + fr: "Aucune navigation possible lorsque le mode synchro est activé.", + nl: "Geen navigatie mogelijk terwijl de synchronisatiemodus is ingeschakeld."}, + "Press S to toggle sync mode off.": { + de: "Drücken Sie S, um den Sync-Modus auszuschalten.", + fr: "Appuyez sur S pour désactiver le mode synchro.", + nl: "Druk op S om de synchronisatiemodus uit te schakelen."}, + "Synchronization error.": { + de: "Synchronisierungsfehler", + fr: "Erreur de synchronisation.", + nl :"Synchronisatiefout."}, + "You can try to turn synchronization back on with the S key.": { + de: "Sie können versuchen, die Synchronisation mit der Taste S wieder einzuschalten.", + fr: "Vous pouvez essayer de réactiver la synchronisation avec la touche S.", + nl: "U kunt proberen de synchronisatie weer in te schakelen met de S-toets."}, + "An error occurred while trying to switch into fullscreen mode": { + de: "Beim Wechsel in den Vollbildmodus ist ein Fehler aufgetreten", + fr: "Une erreur s'est produite en essayant de passer en mode plein écran", + nl: "Er is een fout opgetreden bij het overschakelen naar volledig scherm"}, + "Fullscreen mode is not possible": { + de: "Der Vollbildmodus ist nicht möglich", + fr: "Le mode plein écran est impossible", + nl: "Volledig scherm is niet mogelijk"}, + "You can try again with the F or F1 key.": { + de: "Sie können es mit der Taste F oder F1 erneut versuchen.", + fr: "Vous pouvez réessayer avec la touche F ou F1.", + nl: "U kunt het opnieuw proberen met de toets F of F1."}, + "Syncing turned OFF.\nPress S to turn syncing back on.": { + de: "Synchronisierung ausgeschaltet.\nDrücken Sie S, um die Synchronisierung wieder einzuschalten.", + fr: "Synchronisation désactivée\nAppuyez sur S pour réactiver la synchronisation,", + nl: "Synchroniseren uitgeschakeld\nDruk op S om het synchroniseren weer in te schakelen"}, + "Syncing turned ON\nPress S to turn syncing off": { + de: "Synchronisierung eingeschaltet\nDrücken Sie S, um die Synchronisierung auszuschalten", + fr: "Synchronisation activée\nAppuyez sur S pour désactiver la synchronisation", + nl: "Synchronisatie ingeschakeld\nDruk op S om synchronisatie uit te schakelen"}, + "Mouse & keyboard commands": { + de: "Maus- und Tastaturbefehle", + fr: "Commandes de la souris et du clavier", + nl: "Muis- en toetsenbordopdrachten"}, + "A, double click, 3-finger touch": { + de: "A, Doppelklick, 3-Finger-Touch", + fr: "A, double clic, toucher à 3 doigts", + nl: "A, dubbelklik, 3-vinger touch"}, + "enter slide mode": { + de: "Dia-Modus einschalten", + fr: "passer en mode diapo", + nl: "naar de diamodus gaan"}, + "A, Esc, 3-finger touch": { + de: "A, Esc, 3-Finger-Touch", + fr: "A, Esc, toucher à 3 doigts", + nl: "A, Esc, 3-vinger touch"}, + "leave slide mode": { + de: "Dia-Modus ausschalten", + fr: "quiter le mode diapo", + nl: "diamodus verlaten"}, + "space, , , swipe left": { + de: "Leertaste, , , links wischen", + fr: "espace, , , glisser vers la gauche", + nl: "spatie, , , veeg naar links", + }, + "space, , , click": { + de: "Leertaste, , , click", + fr: "espace, , , clic", + nl: "spatie, , , klik"}, + "next slide or incremental element": { + de: "nächstes Dia oder inkrementelles Element", + fr: "diapo suivante ou élément incrémentiel", + nl: "volgende dia of incrementeel element"}, + "PgDn": {}, + "PgDn, swipe left": { + de: "PgDn, links wischen", + fr: "PgDn, glisser vers la gauche", + nl: "PgDn, veeg naar links"}, + "next slide": { + de: "nächstes Dia", + fr: "diapo suivante", + nl: "volgende dia"}, + "PgUp, , , swipe right": { + de: "PgUp, , , rechts wischen", + fr: "PgUp, , , glisser vers la droite", + nl: "PgUp, , , veeg naar rechts"}, + "previous slide": { + de: "vorheriges Dia", + fr: "diapo précédente", + nl: "vorige dia"}, + "End": {}, + "last slide": { + de: "letztes Dia", + fr: "dernière diapo", + nl: "laatste dia"}, + "Home": {}, + "first slide": { + de: "erstes Dia", + fr: "première diapo", + nl: "eerste dia"}, + "F1, F": {}, + "toggle fullscreen mode": { + de: "Vollbildmodus umschalten", + fr: "basculer le mode plein écran", + nl: "volledig scherm aan/uit", + }, + "2": {}, + "show slides in 2nd window": { + de: "abspielen in 2. Fenster", + fr: "lire dans 2e fenêtre", + nl: "afspelen in 2e venster"}, + "?": {}, + "this help": { + de: "diese Hilfe", + fr: "cette aide", + nl: "deze hulp"}, + "S": {}, + "toggle sync mode on/off": { + de: "Sync-Modus ein-/ausschalten", + fr: "activer/désactiver le mode synchro", + nl: "sync-modus aan/uit"}, + "(More information in the b6+ manual)": { + de: "(Weitere Informationen im b6+ Handbuch)", + fr: "(Plus d'informations dans le manuel de b6+)", + nl: "(Meer informatie in de b6+ handleiding)"}, + "▶\uFE0E": {}, + "play slides or stop playing": { + de: "Dias abspielen oder halten", + fr: "lancer les diapos ou arrêter", + nl: "dia's afspelen of stoppen"}, + "play/stop": { + de: "abspielen/halten", + fr: "lire/arrêter", + nl: "afspelen/stoppen"}, + "⧉": {}, + "play in 2nd window": { + de: "abspielen in 2. Fenster", + fr: "lire dans 2eme fenêtre", + nl: "afspelen in 2de venster"}, + "play/stop slides in a 2nd window": { + de: "Dias abspielen/halten in einem zweiten Fenster", + fr: "lancer/arrêter les diapos dans une 2eme fenêtre", + nl: "dia's afspelen/stoppen in een 2de venster"}, + "❮": {}, + "back": { + de: "zurück", + fr: "précédent", + nl: "terug"}, + "❯": {}, + "forward": { + de: "vorwärts", + fr: "suivant", + nl: "vooruit"}, + "?": {}, + "help": { + de: "Hilfe", + fr: "aide", + nl: "help"}, + "◑": {}, + "dark mode": { + de: "Dunkel­modus", + fr: "mode sombre", + nl: "donkere modus"}, + "toggle dark mode on/off": { + de: "Dunkelmodus ein- oder ausschalten", + fr: "activer ou désactiver le mode sombre", + nl: "schakel de donkere modus aan of uit"}, +}; + +/* Global variables */ +var curslide = null; +var slidemode = false; // In slide show mode or normal mode? +var switchInProgress = false; // True if waiting for finishToggleMode() +var incrementals = null; // Array of incrementally displayed items +var gesture = {}; // Info about touch/pointer gesture +var numslides = 0; // Number of slides +var stylesToLoad = 0; // # of load events to wait for +var limit = 0; // A time limit used by toggleMode() +var nextid = 0; // For generating unique IDs +var interactive = true; // Allow navigating to a different slide? +var fullmode = false; // Whether "?full" was in the URL +var progressElts = []; // Elements with class=progress +var slidenumElts = []; // Elements with class=slidenum +var liveregion = null; // Element [role=region][aria-live=assertive] +var savedContent = ""; // Initial content of the liveregion +var noclick = false; // If true, mouse clicks do not advance slides +var hideMouseTime = null; // If set, hide idle mouse pointer after N ms +var helptext = null; // List of keyboard and mouse commands +var hideMouseID = null; // ID of timer to hide the mouse pointer +var singleClickTimer = null; // Timeout to distinguish single & double click +var secondwindow = null; // Second window for speaker notes +var firstwindow = null; // The window that opened this one +var syncmode = false; // Sync mode +var syncURL = null; // URL of sync server +var eventsource = null; // Sync server object +var startTime = 0; // Start time, used by displayed clocks +var pauseStartTime = 0; // 0 = clocks not paused, > 0 = start of pause +var realHoursElts = null; // Elements with wallclock time: hours +var realMinutesElts = null; // Elements with wallclock time: minutes +var realSecondsElts = null; // Elements with wallclock time: seconds +var usedHoursElts = null; // Elements with used time: hours +var usedMinutesElts = null; // Elements with used time: minutes +var usedSecondsElts = null; // Elements with used time: seconds +var leftHoursElts = null; // Elements with remaining time: hours +var leftMinutesElts = null; // Elements with remaining time: minutes +var leftSecondsElts = null; // Elements with remaining time: seconds +var clockTimer = 0; // Interval timer for clocks +var duration = 30 * 60 * 1000; // Default duration of a presentation 30 min +var warnTime = 5 * 60 * 1000; // Warn 5 minutes before end of duration +var language = null; // Language for localization +var switchFullscreen = false; // True = toggle fullscreen but not slide mode +var hasDarkMode = false; // Style sheet supports class=darkmode? +var incrementalsBehavior = "freeze"; // [Experimental] + + +/* _ -- return translation for text, or text, if none is available */ +function _(text) +{ + return translations[text]?.[language] ?? text; +} + + +/* generateID -- make sure elt has a unique ID */ +function generateID(elt) +{ + /* This doesn't guarantee that elt has a unique ID, but only that it + * is the first element in the document that has this ID. Which + * should be enough to make this element scroll into view when it is + * the target... */ + if (!elt.id) elt.id = "s" + ++nextid; + while (document.getElementById(elt.id) !== elt) elt.id = "s" + ++nextid; +} + + +/* cloneNodeWithoutID -- deep clone a node, but not any ID attributes */ +function cloneNodeWithoutID(elt) +{ + var clone, h; + + clone = elt.cloneNode(false); + if (elt.nodeType === 1 /*Node.ELEMENT_NODE*/) { + clone.removeAttribute("id"); // If any + for (h = elt.firstChild; h; h = h.nextSibling) + clone.appendChild(cloneNodeWithoutID(h)); // Recursive + } + return clone; +} + + +/* ticker -- update clock elements */ +function ticker(force) +{ + var now, s0, m0, h0, s1, m1, h1, s2, m2, h2, used, left; + + // This function is usually called as interval time handler, but is + // also called when the clocks need to be updated, e.g., after a change + // in duration or pause/resume. + + now = new Date(); + + s0 = now.getSeconds(); + m0 = now.getMinutes(); + h0 = now.getHours(); + + for (const e of realHoursElts) + e.textContent = h0.toString().padStart(2, "0"); + for (const e of realMinutesElts) + e.textContent = m0.toString().padStart(2, "0"); + for (const e of realSecondsElts) + e.textContent = s0.toString().padStart(2, "0"); + + if (pauseStartTime === 0 || force) { // Clocks aren't paused or update forced + + used = now.getTime() - startTime; + s1 = Math.trunc(used / 1000); + if (usedHoursElts.length != 0) { // Used hours are displayed + h1 = Math.trunc(s1 / 60 / 60); s1 -= h1 * 60 * 60; + m1 = Math.trunc(s1 / 60); s1 -= m1 * 60; + } else if (usedMinutesElts.length != 0) { // No hours, but minutes are shown + m1 = Math.trunc(s1 / 60); s1 -= m1 * 60; + } + for (const e of usedHoursElts) + e.textContent = h1.toString().padStart(2, "0"); + for (const e of usedMinutesElts) + e.textContent = m1.toString().padStart(2, "0"); + for (const e of usedSecondsElts) + e.textContent = s1.toString().padStart(2, "0"); + + left = Math.max(0, duration - used); + s2 = Math.trunc(left / 1000); + if (leftHoursElts.length != 0) { // Remaining hours are displayed + h2 = Math.trunc(s2 / 60 / 60); s2 -= 60 * 60 * h2; + m2 = Math.trunc(s2 / 60); s2 -= 60 * m2; + } else if (leftMinutesElts.length) { // No hours, but minutes are shown + m2 = Math.trunc(s2 / 60); s2 -= 60 * m2; + } + for (const e of leftHoursElts) + e.textContent = h2.toString().padStart(2, "0"); + for (const e of leftMinutesElts) + e.textContent = m2.toString().padStart(2, "0"); + for (const e of leftSecondsElts) + e.textContent = s2.toString().padStart(2, "0"); + + // Set a precise factor 0.0..1.0 in a CSS variable on BODY. + // Set an integer percentage 00..100 in a data attribute on BODY. + // If time left is <= warnTime, set class=time-warning on BODY. + document.body.style.setProperty("--time-factor", 1 - left/duration); + document.body.setAttribute("data-time-factor", + Math.trunc(100 * (1 - left/duration)).toString().padStart(2, "0")); + if (left <= warnTime) document.body.classList.add("time-warning"); + else document.body.classList.remove("time-warning"); + } +} + + +/* addMinute -- add 1 minute to the duration */ +function addMinute(ev) +{ + duration += 60000; + + if (firstwindow) + firstwindow.postMessage({event: "duration", v: duration}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "duration", v: duration}); + + + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* subtractMinute -- subtract 1 minute from the duration */ +function subtractMinute(ev) +{ + duration = Math.max(0, duration - 60000); + + if (firstwindow) + firstwindow.postMessage({event: "duration", v: duration}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "duration", v: duration}); + + ticker(true); // Update the clocks + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* pauseTime -- pause or resume the clocks */ +function pauseTime(ev) +{ + if (pauseStartTime) { // We're resuming, add paused time to startTime + startTime += Date.now() - pauseStartTime; + pauseStartTime = 0; + document.body.classList.remove("paused"); + } else { // We're pausing, remember start time of pause + pauseStartTime = Date.now(); + document.body.classList.add("paused"); + } + + ticker(true); // Update the clocks + + if (firstwindow) { + firstwindow.postMessage({event: "startTime", v: startTime}); + firstwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + } else if (secondwindow?.closed === false) { + secondwindow.postMessage({event: "startTime", v: startTime}); + secondwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + } + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* resetTime -- restart the clock */ +function resetTime(ev) +{ + startTime = Date.now(); + + if (firstwindow) + firstwindow.postMessage({event: "startTime", v: startTime}); + else if (secondwindow?.closed === false) + secondwindow.postMessage({event: "startTime", v: startTime}); + + ticker(true); // Update the clocks + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* ignoreEvent -- cancel an event */ +function ignoreEvent(ev) +{ + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* initClocks -- find and initialize clock elements */ +function initClocks() +{ + var t; + + // Get the duration and warn time of the presentation from body.class. + for (const c of document.body.classList) { + if ((t = c.match(/^duration=([0-9.]+)$/))) duration = 1000 * 60 * t[1]; + if ((t = c.match(/^warn=([0-9.]+)$/))) warnTime = 1000 * 60 * t[1]; + } + + // If there are elements with class=fullclock or class=clock + // and that don't have child elements already, fill them with + // appropriate elements to make a clock. + for (const c of document.getElementsByClassName("fullclock")) + if (!c.firstElementChild) + c.insertAdjacentHTML("beforeend", '' + _('current time') + '' + + '' + + '' + + '' + _('used') + '' + + '' + + '' + _('remaining') + '' + + '' + + '' + + '' + + '' + + ''); + for (const c of document.getElementsByClassName("clock")) + if (!c.firstElementChild) + c.insertAdjacentHTML("beforeend", + '' + + '' + + '' + + '' + + '' + + ''); + + // Find all elements that will contain time. + realHoursElts = document.getElementsByClassName("hours-real"); + realMinutesElts = document.getElementsByClassName("minutes-real"); + realSecondsElts = document.getElementsByClassName("seconds-real"); + usedHoursElts = document.getElementsByClassName("hours-used"); + usedMinutesElts = document.getElementsByClassName("minutes-used"); + usedSecondsElts = document.getElementsByClassName("seconds-used"); + leftHoursElts = document.getElementsByClassName("hours-remaining"); + leftMinutesElts = document.getElementsByClassName("minutes-remaining"); + leftSecondsElts = document.getElementsByClassName("seconds-remaining"); + + // Find all elements that adjust the clock and install event handlers. + for (const e of document.getElementsByClassName("timeinc")) { + e.addEventListener("click", addMinute, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timedec")) { + e.addEventListener("click", subtractMinute, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timepause")) { + e.addEventListener("click", pauseTime, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + for (const e of document.getElementsByClassName("timereset")) { + e.addEventListener("click", resetTime, true); + e.addEventListener("dblclick", ignoreEvent, true); + } + + // Install a timer to update the clock elements, if needed. + if (realHoursElts.length || realMinutesElts.length || + realSecondsElts.length || usedHoursElts.length || + usedMinutesElts.length || usedSecondsElts.length || + leftHoursElts.length || leftMinutesElts.length || + leftSecondsElts.length) + clockTimer = setInterval(ticker, 1000, false); // Clock tick every second + + // Remember start time of presentation. + if (clockTimer) startTime = Date.now(); +} + + +/* initIncrementals -- find incremental elements in current slide */ +function initIncrementals() +{ + var e = curslide; + + // Collect all incremental elements into array incrementals. + // + // The functions nextSlideOrElt() and previousSlideOrElt() maintain + // the following invariant: If there are incrementals, there is at + // most one of them with a class of "active". If there is an + // "active" element, all incrementals before it, and only those, + // have a class of "visited". If there is an "active" element, + // incrementals[incrementals.cur] points to that element; if there + // is not, incrementals.cur is -1. + // + // incrementalsBehavior is an experimental variable to evaluate + // different behaviors when going backwards inside a slide with + // incremental elements: + // + // "freeze": When you leave a slide, the incremental elements that + // are currently displayed become frozen. When going back to that + // slide, those elements are still displayed but can no longer be + // removed by pressing the left arrow. This is the behavior of + // Shower and is currently the default. + // + // "reset": Every time you enter a slide, all incremental elements + // are in their hidden state. E.g., if you leave a slide with all + // elements visible and then go back, all elements are hidden again. + // + // "symmetric": When you return to a slide, the slide is exactly as + // you left it. Incremental elements that were displayed when you + // left the slide are still displayed and can be hidden by pressing + // the left arrow. + // + // "forwardonly": When you enter a slide, all incremental elements + // are in their hidden state (as with "reset"). In addition, + // pressing the left arrow when some incremental elements are + // displayed, resets all elements to their hidden state. + // + // Note that will all of these except "symmetric", the left arrow + // acts very much like the PageUp key: when you go back to the + // previous slide, every next press of the left arrow goes back one + // slide. + // + incrementals = []; + incrementals.cur = -1; + do { + /* Go to the next node, in document source order. */ + if (e.firstChild) { + e = e.firstChild; + } else { + while (e && !e.nextSibling) e = e.parentNode; + if (e) e = e.nextSibling; + } + if (!e) break; /* End of document */ + if (e.nodeType != 1) continue; /* Not an element */ + if (e.b6slidenum) break; /* Reached the next slide */ + if (e.classList.contains("incremental") || e.classList.contains("overlay")) + for (const c of e.children) + if (incrementalsBehavior === "symmetric") { + if (c.classList.contains("active")) + incrementals.cur = incrementals.length; // Start at this element + incrementals.push(c); + } else if (incrementalsBehavior === "reset" || + incrementalsBehavior == "forwardonly") { + c.classList.remove("active"); + c.classList.remove("visited"); + incrementals.push(c); + } else { // "freeze" + if (!c.classList.contains("visited") && + !c.classList.contains("active")) + incrementals.push(c); + } + if (e.classList.contains("next")) { /* It is an incremental element */ + if (incrementalsBehavior === "symmetric") { + if (e.classList.contains("active")) + incrementals.cur = incrementals.length; // Start at this element + incrementals.push(e); + } else if (incrementalsBehavior === "reset" || + incrementalsBehavior == "forwardonly") { + e.classList.remove("active"); + e.classList.remove("visited"); + incrementals.push(e); + } else { // "freeze" + if (!e.classList.contains("visited") && + !e.classList.contains("active")) + incrementals.push(e); + } + } + } while (1); +} + + +/* isStartOfSlide -- check if element has class=slide, page-break or is an H1 */ +function isStartOfSlide(elt) +{ + if (elt.nodeType != 1) return false; // Not an element + if (elt.classList.contains("slide")) return true; + if (window.getComputedStyle(elt).getPropertyValue('page-break-before') == + 'always') return true; + if (elt.nodeName != "H1") return false; + + /* The element is an H1. It starts a slide unless it is inside class=slide */ + while (true) { + elt = elt.parentNode; + if (!elt || elt.nodeType != 1) return true; + if (elt.classList.contains("slide")) return false; + } +} + + +/* updateProgress -- update the progress bars and slide numbers, if any */ +function updateProgress() +{ + var p = curslide.b6slidenum / numslides; + + /* Set the width of the progress bars */ + for (const e of progressElts) e.style.width = 100 * p + "%"; + + /* Set the content of .slidenum elements to the current slide number */ + for (const e of slidenumElts) e.textContent = curslide.b6slidenum; + + /* Set a custom variable on BODY for use by style rules */ + document.body.style.setProperty("--progress", p); +} + + +/* initProgress -- find .progress and .slidenum elements, count slides */ +function initProgress() +{ + var s; + + /* Find all elements that are progress bars, unhide them. */ + progressElts = document.getElementsByClassName("progress"); + for (const e of progressElts) + if (typeof e.b6savedstyle === "string") e.style.cssText = e.b6savedstyle; + + /* Find all that should contain the current slide number, unhide them. */ + slidenumElts = document.getElementsByClassName("slidenum"); + for (const e of slidenumElts) + if (typeof e.b6savedstyle === "string") e.style.cssText = e.b6savedstyle; + + /* Find all that should contain the # of slides, fill and unhide them. */ + for (const e of document.getElementsByClassName("numslides")) { + if (typeof e.b6savedstyle == "string") e.style.cssText = e.b6savedstyle; + e.textContent = numslides; // Set content to number of slides + } + + /* Set the # of slides in a CSS counter on the BODY. */ + s = window.getComputedStyle(document.body).getPropertyValue("counter-reset"); + if (s === "none") s = ""; else s += " "; + document.body.style.setProperty('counter-reset',s + 'numslides ' + numslides); +} + + +/* numberSlides -- count slides, number them, and make sure they have IDs */ +function numberSlides() +{ + numslides = 0; + for (const h of document.body.children) + if (isStartOfSlide(h)) { + h.b6slidenum = ++numslides; // Save number in element + generateID(h); // If the slide has no ID, add one + } +} + + +/* hideMouse -- make the mouse pointer invisible (only in slide mode) */ +function hideMouse() +{ + if (slidemode) document.body.style.cursor = 'none'; + hideMouseID = 0; // 0 = timer has fired, cursor is hidden +} + + +/* hideMouseReset -- event handler for mousemove to reset the hideMouse timer */ +function hideMouseReset() +{ + if (hideMouseID === 0) { // Timer has fired and hid the cursor. Unhide it. + document.body.style.cursor = null; + hideMouseID = null; // null = cursor is visible + } else if (hideMouseID !== null) { // Timer hasn't fired yet. Remove it. + clearTimeout(hideMouseID); + hideMouseID = null; // null = cursor is visible + } + + /* If still in slide mode, set a new timer; otherwise remove ourselves. */ + if (slidemode) hideMouseID = setTimeout(hideMouse, hideMouseTime); + else document.removeEventListener('mousemove', hideMouseReset); +} + + +/* initHideMouse -- set a timeout to hide the mouse pointer when it is idle */ +function initHideMouse() +{ + if (hideMouseTime === null) return; + + /* Add handler to restart the timer when the mouse moves. */ + document.addEventListener('mousemove', hideMouseReset); + + /* Remove old timer, unhide cursor if hidden, start new timer. */ + hideMouseReset(); +} + + +/* displaySlide -- make the current slide visible */ +function displaySlide() +{ + var h, url; + + /* curslide has class=slide, page-break-before=always or is an H1 */ + curslide.style.cssText = curslide.b6savedstyle; + curslide.classList.add("active"); // Compatibility with Shower + liveregion.innerHTML = ""; // Make it empty + + if (!curslide.classList.contains('slide')) { + liveregion.appendChild(cloneNodeWithoutID(curslide)); + /* Unhide all elements until the next slide. And copy the slide to + the live region so that it is spoken */ + for (h = curslide.nextSibling; h && ! h.b6slidenum; h = h.nextSibling) + if (h !== liveregion) { + if (h.nodeType === 1) h.style.cssText = h.b6savedstyle; + liveregion.appendChild(cloneNodeWithoutID(h)); + } + + } else { // class=slide + /* Copy the contents of the slide to the live region so that it is spoken */ + for (h = curslide.firstChild; h; h = h.nextSibling) + liveregion.appendChild(cloneNodeWithoutID(h)); + } + + updateProgress(); + initIncrementals(); + + /* If there is a first window, tell it to scroll to the same slide. */ + if (firstwindow) + firstwindow.postMessage({event: "slide", + v: curslide.id || curslide.b6slidenum}, "*"); + + /* If the fragment ID is not the ID of the current slide, remove it. */ + if (curslide.id && location.hash && curslide.id != location.hash.substring(1)) + location.hash = ""; +} + + +/* hideSlide -- make the current slide invisible */ +function hideSlide() +{ + var h; + + if (!curslide) return; + + /* curslide has class=slide, page-break-before=always or is an H1 */ + curslide.classList.remove("active"); // Compatibility with Shower + curslide.classList.add("visited"); // Compatibility with Shower + curslide.style.visibility = "hidden"; + curslide.style.position = "absolute"; + curslide.style.top = "0"; + for (h = curslide.nextSibling; h && ! h.b6slidenum; h = h.nextSibling) + if (h.nodeType === 1 /*Node.ELEMENT_NODE*/ && h !== liveregion) { + h.style.visibility = "hidden"; + h.style.position = "absolute"; + h.style.top = "0"; + } +} + + +/* makeCurrent -- hide the previous slide, if any, and display elt */ +function makeCurrent(elt) +{ + console.assert(elt); + if (curslide != elt) { + hideSlide(); + curslide = elt; + displaySlide(); + } +} + + +/* fullscreen -- toggle fullscreen mode or turn it on ("on") or off ("off") */ +function toggleFullscreen(onoff) +{ + switchFullscreen = true; // For the fullscreenchange event handler + + if (onoff !== "on" && document.fullscreenElement) + document.exitFullscreen(); + else if (onoff !== "on" && document.webkitFullscreenElement) + document.webkitExitFullscreen(); + else if (onoff !== "off" && document.fullscreenEnabled) + document.documentElement.requestFullscreen({navigationUI: "hide"}) + .catch(err => { + alert(_("An error occurred while trying to switch into fullscreen mode") + + ' (' + err.message + ' – ' + err.name + ")\n\n" + + _("You can try again with the F or F1 key."))}); + else if (onoff !== "off" && document.documentElement.webkitRequestFullscreen) + document.documentElement.webkitRequestFullscreen(); + else if (onoff !== "off") + window.alert(_("Fullscreen mode is not possible")); +} + + +/* createHelpText -- fill the helptext element with help text */ +function createHelpText() +{ + var iframe, button; + + /* Put the help text in an IFRAME so it is not affected by the slide style */ + iframe = document.createElement('iframe'); + iframe.srcdoc = + "" + + "" + + "" + + "" + + "" + + "" + + (syncmode ? "" : + "
" + _("Mouse & keyboard commands") + "
" + _("A, double click, 3-finger touch") + + "" + _("enter slide mode") + + "
" + _("A, Esc, 3-finger touch") + + "" + _("leave slide mode") + + "
" + + (noclick ? + _("space, , , swipe left") : + _("space, , , click")) + + "" + _("next slide or incremental element") + + "
" + + (noclick ? _("PgDn") : _("PgDn, swipe left")) + + "" + _("next slide") + + "
" + + _("PgUp, , , swipe right") + + "" + _("previous slide") + + "
" + _("End") + + "" + _("last slide") + + "
" + _("Home") + + "" + _("first slide") + + "
" + _("F1, F") + + "" + _("toggle fullscreen mode") + + "
" + _("2") + "" + + _("show slides in 2nd window") + + (!hasDarkMode ? "" : + "
" + _("D") + + "" + _("toggle dark mode on/off")) + + "
" + _("?") + + "" + _("this help")) + + (!syncURL ? "" : + "
" + _("S") + + "" + _("toggle sync mode on/off")) + + "
" + + "

" + _("(More information in the b6+ manual)"); + iframe.style.cssText = 'margin: 0; border: none; padding: 0; ' + + 'width: 100%; height: 100%'; + button = document.createElement('button'); + button.innerHTML = "\u274C\uFE0E"; // Cross mark + button.style.cssText = 'position:absolute; top: 0; right: 16px'; + button.addEventListener('click', + ev => {document.body.removeChild(helptext); ev.stopPropagation()}); + // Unfortunately, when in fullscreen mode, the Escape key is + // captured by the browser to exit fullscreen mode and we never get + // it. + button.setAttribute("tabindex", 0); + button.addEventListener('keydown', ev => { + if (ev.key == "Escape") {helptext.remove(); ev.stopPropagation()}}); + helptext = document.createElement('div'); + helptext.appendChild(iframe); + helptext.appendChild(button); + helptext.style.cssText = 'position: fixed; width: 100%; height: 100%; ' + + 'top: 0; left: 0; z-index: 2; background: #000; color: #FFF; ' + + 'text-align: center; visibility: visible'; + // In fullscreen mode, the Escape key is captured by the browser to + // leave fullscreen mode, so only the second Escape press closes the + // help text. But let's add it anyway. + helptext.setAttribute("tabindex", 0); + helptext.addEventListener('keydown', ev => { + if (ev.key == "Escape") {helptext.remove(); ev.stopPropagation()}}); +} + + +/* help -- show information about available interactive commands */ +function help() +{ + // Works both on first and second wondows + if (!helptext) createHelpText(); + document.body.appendChild(helptext); + helptext.lastChild.focus(); // The button +} + + +/* openSecondWindow -- open a 2nd window with the same slides */ +function openSecondWindow() +{ + var url; + + console.assert(!firstwindow); // We're on the first window + + // If we're in slide mode, go back to index mode. + // This should never happen: the "2" key is refused in slide mode + // and the button is invisible in slide mode. + if (slidemode) toggleMode(); + + // Open a second window if there isn't one yet. The + // "?b6window=random" avoids that this document replaces the + // original slides in the browser cache. + if (secondwindow == null || secondwindow.closed) { + url = new URL(location); + url.searchParams.delete("full"); + url.searchParams.delete("static"); + url.searchParams.delete("sync"); + url.searchParams.set("b6window", + Math.trunc(0x100000 * Math.random()).toString(36)); + // url.hash = ""; + secondwindow = open(url, "b6+ slide window", + "innerWidth=800,innerHeight=690"); + secondwindow.focus(); + } + + // The second window will send us an "init" message when it is + // ready. At that point we'll send it some information about our + // clocks and the currently active slide, if any. See message() + // below. +} + + +/* warnSyncMode -- alert the user that sync mode is on */ +function warnSyncMode() +{ + console.assert(!firstwindow); // We're on the first window + warningBanner(_("No navigation possible while sync mode is on."), "\n", + _("Press S to toggle sync mode off.")); +} + + +/* warningBanner -- briefly show a banner with a warning */ +function warningBanner(...content) +{ + var banner, elt; + + banner = document.createElement("div"); + banner.style = "position: fixed; left: 0; right: 0; z-index: 2;\ + text-align: center; font-size: 2vh; font-weight: bold;\ + white-space: pre-line;\ + font-family: sans-serif; margin: 0; padding: 0.5em; border-style: none;\ + background: hsla(0,0%,0%,0.6); color: hsl(0,0%,100%);\ + text-shadow: 1px 1px 1px #000, 1px 1px 1px #000; opacity: 1.0;\ + transition: opacity 3.0s"; + if (slidemode) {banner.style.top = "auto"; banner.style.bottom = "0";} + else {banner.style.top = "0"; banner.style.bottom = "auto";} + banner.append(...content); + document.body.append(banner); + + // First let the style transition fade the dialog, then remove it. + setTimeout(function () {banner.style.opacity = "0.0"}, 3000); + setTimeout(function () {banner.remove()}, 6000); +} + + +/* errorSyncMode -- show an error message when synchronization fails */ +function errorSyncMode(ev) +{ + warningBanner(_("Synchronization error."), "\n", + _("You can try to turn synchronization off and on again with the S key.")); +} + + +/* tryToggleSync -- toggle sync mode on or off, if possible */ +function tryToggleSync() +{ + console.assert(!firstwindow); // We're on the 1st window + + if (!syncURL) return; // No sync server defined + + if (syncmode) { + eventsource.close(); + syncmode = false; + secondwindow?.postMessage({event: "sync-off"}); + warningBanner(_("Syncing turned OFF.\nPress S to turn syncing back on.")); + } else { + eventsource = new EventSource(syncURL); + // Listen both for "message" events (the default type) and "page" events + eventsource.addEventListener("message", syncHandler); + eventsource.addEventListener("page", syncHandler); + eventsource.addEventListener("error", errorSyncMode); + // eventsource.addEventListener("open", function (ev) {)}); + // Don't wait for the "open" event. It seems some browsers (Safari + // and Firefox, but not Vivaldi) don't emit the event until much + // later. (When the first message arrives?) + syncmode = true; + secondwindow?.postMessage({event: "sync-on"}); + warningBanner(_("Syncing turned ON\nPress S to turn syncing off")); + } +} + + +/* keyDown -- handle key presses on the BODY element */ +function keyDown(event) +{ + // We only handle the key if it is not directed at a focused element. + if (event.target.tagName !== "BODY") return; + + // We don't handle keys when a modifier key is pressed. + if (event.altKey || event.ctrlKey || event.metaKey) return; + + switch (event.key) { + case "PageDown": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) nextSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow?.postMessage({event: "keydown", v: event.key}); + break; + case "PageUp": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) previousSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Spacebar": // Some older browsers + case " ": // Fall through + case "Right": // Some older browsers + case "ArrowRight": // Fall through + case "Down": // Some older browsers + case "ArrowDown": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) nextSlideOrElt() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Left": // Some older browsers + case "ArrowLeft": // Fall through + case "Up": // Some older browsers + case "ArrowUp": // Fall through + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) previousSlideOrElt() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "Home": + if (syncmode) warnSyncMode() // In sync mode: accept key, do nothing + else if (slidemode) firstSlide() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "End": + if (syncmode) warnSyncMode() + else if (slidemode) lastSlide() + else if (secondwindow?.closed !== false) return + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "a": // Accepted even when not in slide mode + if (syncmode) warnSyncMode() + else if (secondwindow?.closed !== false) toggleModeAndFullscreen() + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "f": // Fall through + case "F1": + if (slidemode) toggleFullscreen() // In slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + // TODO: This does not work: the 2nd window will not go into + // fullscreen, for security reasons. Show a message here instead + // with instructions? + break; + case "Esc": // Some older browsers + case "Escape": + if (syncmode) warnSyncMode() + else if (slidemode) toggleModeAndFullscreen() + else if (secondwindow?.closed !== false) return + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + case "2": + if (slidemode) return; // Only one window can be in slide mode + else if (syncmode) warnSyncMode() + else if (!firstwindow) openSecondWindow(); + else return; // We're on the 2nd window. Ignore key + break; + case "?": + help(event); + break; + case 's': + if (syncURL) tryToggleSync() // On 1st window, sync server defined + else if (!firstwindow) return // On 1st window, but no sync server + else firstwindow.postMessage({event: "keydown", v: event.key}); + break; + case 'd': + if (slidemode) toggleDarkMode() // We're in slide mode + else if (secondwindow?.closed !== false) return // No 2nd window + else secondwindow.postMessage({event: "keydown", v: event.key}); + break; + default: + return; // Other keys have their normal meaning. + } + + event.preventDefault(); +} + + +/* load -- handle the load event */ +function load(e) +{ + if (stylesToLoad) stylesToLoad--; + e.target.removeEventListener(e.type, load); +} + + +/* toggleMedia -- swap styles for projection and screen */ +function toggleMedia() +{ + var i, h, s, links, styles; + + var re1 = /\(\s*overflow-block\s*:\s*((optional-)?paged)\s*\)/gi; + var sub1 = "(min-width: 0) /* $1 */"; + var re2 = /\(min-width: 0\) \/\* ((optional-)?paged) \*\//gi; + var sub2 = "(overflow-block: $1)"; + var re3 = /\bprojection\b/gi; + var sub3 = "screen"; + var re4 = /\bscreen\b/gi; + var sub4 = "projection"; + + /* Swap projection and screen in MEDIA attributes of LINK elements */ + links = document.getElementsByTagName("link"); + for (i = 0; i < links.length; i++) + if (links[i].rel === "stylesheet" && links[i].media) { + if (re1.test(links[i].media)) s = links[i].media.replace(re1, sub1); + else s = links[i].media.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != links[i].media) { + stylesToLoad++; + links[i].addEventListener('load', load, false); + links[i].media = s; + } + } + + /* Swap projection and screen in MEDIA attributes of STYLE elements */ + styles = document.getElementsByTagName("style"); + for (i = 0; i < styles.length; i++) + if (styles[i].media) { + if (re1.test(styles[i].media)) s = styles[i].media.replace(re1, sub1); + else s = styles[i].media.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != styles[i].media) { + stylesToLoad++; + styles[i].addEventListener('load', load, false); + styles[i].media = s; + } + } + + /* Swap projection and screen in the MEDIA pseudo-attribute of the style PI */ + for (h = document.firstChild; h; h = h.nextSibling) + if (h.nodeType === 7 && h.target === "xml-stylesheet") { + if (re1.test(h.data)) s = h.data.replace(re1, sub1); + else s = h.data.replace(re2, sub2); + if (re3.test(s)) s = s.replace(re3, sub3); + else s = s.replace(re4, sub4); + if (s != h.data) { + stylesToLoad++; + h.addEventListener('load', load, false); // TODO: possible? + h.data = s; + } + } +} + + +/* scaleBody -- if the BODY has a fixed size, scale it to fit the window */ +function scaleBody() +{ + var w, h, scale; + + if (document.body.offsetWidth && document.body.offsetHeight) { + w = document.body.offsetWidth; + h = document.body.offsetHeight; + scale = Math.min(window.innerWidth/w, window.innerHeight/h); + document.body.style.transform = "scale(" + scale + ")"; + document.body.style.position = "relative"; + document.body.style.marginLeft = (window.innerWidth - w)/2 + "px"; + document.body.style.marginTop = (window.innerHeight - h)/2 + "px"; + document.body.style.top = "0"; + document.body.style.left = "0"; + /* --shower-full-scale is for style sheets written for Shower 3.1: */ + document.body.style.setProperty('--shower-full-scale', '' + scale); + } +} + + +/* finishToggleMode -- finish switching to slide mode */ +function finishToggleMode() +{ + if (stylesToLoad != 0 && Date.now() < limit) { + + setTimeout(finishToggleMode, 100); // Wait some more + + } else if (stylesToLoad == 0 && Date.now() < limit) { + + limit = 0; + setTimeout(finishToggleMode, 100); // Wait 100ms for styles to apply + + } else { + + stylesToLoad = 0; + scaleBody(); // If the BODY has a fixed size, scale it to fit the window + initProgress(); // Find and initialize progress bar, etc. + initHideMouse(); // If requested, hide an idle mouse pointer + + // If we're a 2nd window, inform the 1st window that we are ready. + if (firstwindow) firstwindow.postMessage({event: "init"}); + + /* curslide can be set if we reenter slide mode or if doubleClick set it. */ + if (curslide) displaySlide(); + else if (location.hash) targetSlide(location.hash.substring(1)); + else firstSlide(); + + switchInProgress = false; // Done with the mode switch + } +} + + +/* toggleMode -- toggle between slide show and normal display */ +function toggleMode() +{ + /* Do nothing if we are still in the process of switching to slide mode */ + if (switchInProgress) return; + + if (! slidemode) { + switchInProgress = true; + slidemode = true; + document.body.classList.add("full"); // Set .full on BODY + document.body.setAttribute("role", "application"); // Hint to screenreaders + + /* Find or create an element to announce the slides in speech */ + if ((liveregion = + document.querySelector("[role=region][aria-live=assertive]"))) { + savedContent = liveregion.innerHTML; // Remember its content, if any + } else { + liveregion = document.createElement("div"); + liveregion.setAttribute("role", "region"); + liveregion.setAttribute("aria-live", "assertive"); + document.body.appendChild(liveregion); + savedContent = "Stopped."; // Default to an English message + } + + /* Make all children of BODY invisible. */ + for (const h of document.body.children) { + h.b6savedstyle = h.style.cssText; // Remember properties + h.style.visibility = "hidden"; + h.style.position = "absolute"; + h.style.top = "0"; + h.style.left = "0"; + } + + /* Except that the liveregion is visible, but cropped. */ + liveregion.style.visibility = "visible"; + liveregion.style.clip = "rect(0 0 0 0)"; + + /* Swap style sheets for projection and screen. */ + document.body.b6savedstyle = document.body.style.cssText; // Save properties + toggleMedia(); // Swap style sheets + + /* Wait 100ms before calling a function to do the rest of the + initialization of slide mode. That function will wait for the + style sheets to load, but no longer than until limit, i.e., 3 + seconds */ + limit = Date.now() + 3000; + setTimeout(finishToggleMode, 100); + + } else { + + /* savedContent is what a screen reader should say on leaving slide mode */ + liveregion.innerHTML = savedContent; + + /* If there is a first window, tell it we're not in slide mode anymore. */ + if (firstwindow) firstwindow.postMessage({event: "noslide"}); + + // If we're a second window, disappear now. + if (firstwindow) window.close(); + + /* Unhide all children again */ + for (const h of document.body.children) h.style.cssText = h.b6savedstyle; + + toggleMedia(); // Swap style sheets + document.body.style.cssText = document.body.b6savedstyle; // Restore style + document.body.classList.remove("full"); // Remove .full from BODY + document.body.removeAttribute("role"); // Remove "application" + curslide?.classList.remove("active"); // Remove styling + + slidemode = false; + + /* Put current slide in the URL, so the index view can highlight it. */ + if (curslide) location.replace("#" + (curslide.id || curslide.b6slidenum)); + } +} + + +/* toggleModeAndFullscreen -- switch fullscreen slide mode and index mode */ +function toggleModeAndFullscreen() +{ + toggleMode(); + toggleFullscreen(slidemode ? "on" : "off"); +} + + +/* toggleDarkMode -- add, remove or toggle class=darkmode on the BODY */ +function toggleDarkMode(onoff) +{ + var darkmodeIsOn; + + if (! hasDarkMode) return; + + darkmodeIsOn = document.body.classList.contains("darkmode"); + if (darkmodeIsOn && onoff !== "on") { + document.body.classList.remove("darkmode"); + firstwindow?.postMessage({event: "darkmodeOff"}); + secondwindow?.postMessage({event: "darkmodeOff"}); + } else if (! darkmodeIsOn && onoff !== "off") { + document.body.classList.add("darkmode"); + firstwindow?.postMessage({event: "darkmodeOn"}); + secondwindow?.postMessage({event: "darkmodeOn"}); + } +} + + +/* nextSlideOrElt -- next incremental element or next slide if none */ +function nextSlideOrElt() +{ + console.assert(slidemode); + + if (curslide == null) return; + + if (incrementals.cur + 1 < incrementals.length) { + /* There is a next incremental element. */ + + /* Mark the current incremental element, if any, as visited. */ + if (incrementals.cur >= 0) { + incrementals[incrementals.cur].classList.add("visited"); + incrementals[incrementals.cur].classList.remove("active"); + } + + /* Make the next one active. */ + incrementals.cur++; + incrementals[incrementals.cur].classList.add("active"); + + /* Make screen readers announce the newly displayed element */ + liveregion.innerHTML = ""; // Make it empty + liveregion.appendChild(cloneNodeWithoutID(incrementals[incrementals.cur])); + + } else { + /* There is no next incremental element. So go to next slide. */ + nextSlide(); + } +} + + +/* nextSlide -- display the next slide, if any */ +function nextSlide() +{ + var h; + + console.assert(slidemode); + + if (curslide == null) return; + + /* curslide has class=slide, page-break-before=always or is an H1 */ + h = curslide.nextSibling; + while (h && ! h.b6slidenum) h = h.nextSibling; + + if (h != null) makeCurrent(h); +} + + +/* previousSlideOrElt -- next incremental element or next slide if none */ +function previousSlideOrElt() +{ + console.assert(slidemode); + + if (curslide == null) return; + + if (incrementals.cur >= 0) { + // There is an incremental element being displayed. + + // Mark the currently active element as inactive and decrement cur. + incrementals[incrementals.cur--].classList.remove("active"); + // TODO: Remove it from the liveregion. + + if (incrementalsBehavior === "forwardonly") { + // Hide all visited elements and set cur to -1. + while (incrementals.cur >= 0) + incrementals[incrementals.cur--].classList.remove("visited"); + // TODO: Remove them from the liveregion. + } else { + // If there is a preceding incremental element, make it active. + if (incrementals.cur >= 0) { + incrementals[incrementals.cur].classList.remove("visited"); + incrementals[incrementals.cur].classList.add("active"); + } + } + + } else { + // There is no active incremental element. Go to previous slide. + previousSlide(); + } +} + + +/* previousSlide -- display the next slide, if any */ +function previousSlide() +{ + var h; + + console.assert(slidemode); + + if (curslide == null) return; + + console.assert(curslide.b6slidenum); // Element is the start of a slide + + h = curslide.previousSibling; + while (h && ! h.b6slidenum) h = h.previousSibling; + + if (h != null) makeCurrent(h); +} + + +/* firstSlide -- display the first slide */ +function firstSlide() +{ + var h; + + console.assert(slidemode); + + h = document.body.firstChild; + while (h && ! h.b6slidenum) h = h.nextSibling; + + if (h != null) makeCurrent(h); +} + + +/* lastSlide -- display the last slide */ +function lastSlide() +{ + var h; + + console.assert(slidemode); + + h = document.body.lastChild; + while (h && ! h.b6slidenum) h = h.previousSibling; + + if (h != null) makeCurrent(h); +} + + +/* findSlide -- find the slide with the ID or the number "target" */ +function findSlide(target) +{ + var h, n; + + if ((h = document.getElementById(target))) + /* Find enclosing .slide or preceding start of slide */ + while (h && ! h.b6slidenum) h = h.previousSibling || h.parentNode; + else if ((n = parseInt(target)) > 0) + /* Find the start of the n'th slide. */ + for (h = document.body.firstChild; h; h = h.nextSibling) + if (h.b6slidenum === n) break; + + return h; +} + + +/* targetSlide -- display slide containing ID=target, or the target'th slide */ +function targetSlide(target) +{ + var h; + + h = findSlide(target) + /* If found, and it is not already displayed, display it */ + if (h != null) makeCurrent(h); +} + + +/* mouseButtonClick -- handle mouse click event */ +function mouseButtonClick(e) +{ + var target = e.target; + + if (e.button != 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + if (e.detail != 1) return; // It's the 2nd of a double click + + if (syncmode) { // In sync mode, accept the click but do nothing. + + // warnSyncMode(); + + } else { + + // check if target is not something that probably wants clicks + // e.g. embed, object, input, textarea, select, option + while (target) { + if (target.nodeName === "A" || target.nodeName === "EMBED" || + target.nodeName === "OBJECT" || target.nodeName === "INPUT" || + target.nodeName === "TEXTAREA" || target.nodeName === "SELECT" || + target.nodeName === "SUMMARY" || target.nodeName === "OPTION") return; + target = target.parentNode; + } + + if (slidemode) { + // Set a timeout to handle the click after 200 ms. If a double click + // occurs in that period, it will remove the timeout and the click + // will thus not do anything. The 200 ms is a compromise. The actual + // time within which a double click occurs depends on the browser + // and the OS. 200 ms is for fast clickers, but 400 ms would cause a + // noticeable delay before the slide advances. Note that adding + // class=noclick on the body disables handling of single clicks + // completely. + singleClickTimer = setTimeout(() => {nextSlideOrElt()}, 200); + } else if (secondwindow?.closed === false) { + // Not in slide mode, but there is a 2nd window, so let it + // handle the click. + secondwindow.postMessage({event: "click"}); + } + } + + e.preventDefault(); + e.stopPropagation(); +} + + +/* gestureStart -- handle start of a touch event */ +function gestureStart(e) +{ + if (!gesture.on) { + gesture.on = true; + gesture.x2 = gesture.x1 = e.touches[0].clientX; + gesture.y2 = gesture.y1 = e.touches[0].clientY; + gesture.opacity = document.body.style.opacity; + } + gesture.touches = e.touches.length; +} + + +/* gestureMove -- handle move event */ +function gestureMove(e) +{ + if (gesture.on && slidemode) { + gesture.x2 = e.touches[0].clientX; + gesture.y2 = e.touches[0].clientY; + + /* Give some visual feedback: */ + var dx = Math.abs(gesture.x2 - gesture.x1); + var dy = Math.abs(gesture.y2 - gesture.y1); + if (gesture.touches != 1) + document.body.style.opacity = gesture.opacity; + else if (dx > dy) + document.body.style.opacity = 1 - dx / window.innerWidth; + else + document.body.style.opacity = 1 - (6 * dx - 5 * dy) / window.innerWidth; + } +} + + +/* gestureEnd -- handle end of a touch event */ +function gestureEnd(e) +{ + if (gesture.on) { + gesture.on = false; + + /* Undo visual feedback */ + if (slidemode) + document.body.style.opacity = gesture.opacity; + + var dx = gesture.x2 - gesture.x1; + var dy = gesture.y2 - gesture.y1; + + if (gesture.touches > 2) { // 3-finger gesture + if (syncmode) warnSyncMode(); + else if (slidemode) toggleModeAndFullscreen(); // Leave slide mode + else if (secondwindow?.closed !== false) toggleModeAndFullscreen(); + else secondwindow.postMessage({event: "keydown", v: "a"}); + + } else if (gesture.touches == 2) { // 2-finger gesture + return; // does nothing + + } else { // 1-finger swipe + // A swipe can mean previousSlide, nextSlide() or + // nextSlideOrElt(). The latter only if clicks are disabled + // ("noclick"). A swipe in slide mode works directly, a swipe + // while there is a 2nd window sends an event to that window, + // otherwise the swipe is ignored. + if (Math.abs(dx) < window.innerWidth/3) return; // Swipe too short + if (Math.abs(dx) < Math.abs(dy)) return; // Swipe too vertical + if (syncmode) warnSyncMode(); + else if (slidemode) { + if (dx > 0) previousSlide(); + else if (noclick) nextSlideOrElt(); + else nextSlide(); + } else if (secondwindow?.closed === false) { + if (dx > 0) secondwindow.postMessage({event: "keydown", v:"ArrowLeft"}); + else if (noclick) secondwindow.postMessage({event: "keydown", v: " "}); + else secondwindow.postMessage({event: "keydown", v: "PageDown"}); + } + } + e.preventDefault(); + e.stopPropagation(); + } +} + + +/* gestureCancel -- handle cancellation of a touch event */ +function gestureCancel(e) +{ + if (gesture.on) { + gesture.on = false; + /* Undo visual feedback */ + if (slidemode) document.body.style.opacity = gesture.opacity; + } +} + + +/* doubleClick -- handle a double click on the body */ +function doubleClick(event) +{ + var h; + + if (event.button != 0 || event.altKey || event.ctrlKey || + event.metaKey || event.shiftKey) return; + + if (!noclick) { + /* In slide mode, with the mouseButtonClick() handler installed to + * advance the slides on a single click, a double click cancels + * the effect of the single click: It removes the action that + * mouseButtonClick() had put on the queue. */ + clearTimeout(singleClickTimer); + singleClickTimer = null; + } + + /* The double click may have selected some text, so unselect everything. */ + document.getSelection().removeAllRanges(); + + /* Find on which slide, if any, the clicks occurred. */ + h = event.target; + while (h && ! h.b6slidenum) h = h.previousSibling || h.parentNode; + + if (syncmode) { + warnSyncMode(); + } else if (secondwindow?.closed === false) { + // There is 2nd window. If the double click was on a slide, let + // the 2nd window move to that slide, otherwise do nothing. + if (h) secondwindow.postMessage({ event: "dblclick", + v: h ? (h.id || h.b6slidenum) : "" }); + } else if (!slidemode) { + // Enter slide mode. If the double click was on or inside a slide, + // start with that slide. + curslide = h; // May be null + toggleModeAndFullscreen(); + } + + event.preventDefault(); + event.stopPropagation(); +} + + +/* hashchange -- handle fragment id event, make target slide the current one */ +function hashchange(e) +{ + if (slidemode && location.hash) targetSlide(location.hash.substring(1)); +} + + +/* message -- handle a postMessage */ +function message(e) +{ + var newEvent, h; + + if (e.source == secondwindow) { // Message from 2nd window to 1st window + + switch (e.data.event) { + case "init": // Second window has started + document.body.classList.add("has-2nd-window"); + secondwindow.postMessage({event: "startTime", v: startTime}); + secondwindow.postMessage({event: "duration", v: duration}); + secondwindow.postMessage({event: "pauseStartTime", v: pauseStartTime}); + secondwindow.postMessage({event: document.body.classList + .contains("darkmode") ? "darkmodeOn" : "darkmodeOff"}); + break; + case "startTime": // Other window informs us of a new start time + startTime = e.data.v; + ticker(true); // Update the clocks + break; + case "duration": // Other window informs us of a new duration + duration = e.data.v; + ticker(true); // Update the clocks + break; + case "pauseStartTime": // Other window got a pause/resume event + pauseStartTime = e.data.v; + if (pauseStartTime) document.body.classList.add("paused"); + else document.body.classList.remove("paused"); + ticker(true); // Update the clocks + break; + case "keydown": + newEvent = new KeyboardEvent("keydown", {key: e.data.v, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "slide": // Make the slide with given id current + if ((h = findSlide(e.data.v))) { + if (curslide != h) curslide?.classList.remove("active"); + curslide = h; + curslide.classList.add("active"); + curslide.scrollIntoView({behavior: "smooth", block: "center"}); + } + break; + case "noslide": // 2nd window left slide mode + curslide?.classList.remove("active"); + document.body.classList.remove("has-2nd-window"); + // Put current slide in the URL, so the index view can highlight it. + if (curslide) location.replace("#" + (curslide.id||curslide.b6slidenum)); + curslide = null; + break; + case "darkmodeOn": // Second window tells us it entered dark mode + toggleDarkMode("on"); + break; + case "darkmodeOff": // Second window tells us it left dark mode + toggleDarkMode("off"); + break; + } + + } else if (e.source == firstwindow) { // Message from 1st window to 2nd + + switch (e.data.event) { + case "startTime": // 1st window tells us of new start time + startTime = e.data.v; + ticker(true); // Update the clocks + break; + case "duration": // 1st window tells us of new duration + duration = e.data.v; + ticker(true); // Update the clocks + break; + case "pauseStartTime": // 1st window got a pause/resume event + pauseStartTime = e.data.v; + if (pauseStartTime) document.body.classList.add("paused"); + else document.body.classList.remove("paused"); + ticker(true); // Update the clocks + break; + case "click": + newEvent = new MouseEvent("click", {detail: 1, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "dblclick": + newEvent = new MouseEvent("dblclick", {bubbles: true}); + if (e.data.v !== "" && (h = document.getElementById(e.data.v))) + h.dispatchEvent(newEvent); + else + document.body.dispatchEvent(newEvent); + break; + case "keydown": + newEvent = new KeyboardEvent("keydown", {key: e.data.v, bubbles: true}); + document.body.dispatchEvent(newEvent); + break; + case "darkmodeOn": // First window tells us it entered dark mode + toggleDarkMode("on"); + break; + case "darkmodeOff": // First window tells us it left dark mode + toggleDarkMode("off"); + break; + case "sync-on": + syncmode = true; + break; + case "sync-off": + syncmode = false; + break; + case "sync": // Navigate ("+", "-", etc, or a slide ID) + syncSlide(e.data.v); + break; + } + } +} + + +/* windowResize -- handle a resize of the window */ +function windowResize(ev) +{ + if (slidemode) scaleBody(); // Recalculate the transform property +} + + +/* syncSlide -- handle the command string from a server-sent event */ +function syncSlide(command) +{ + if (secondwindow?.closed !== false) { + // There is no 2nd window, or we are ourselves the 2nd window. + // Just after entering slidemode, finishToggleMode() won't have + // run yet and curslide will not be set, which means next and + // previous will do nothing. But that is OK. + // The ":on" and ":off" messages are internal messages, + // generated in message(). + switch (command) { + case "+": if (!slidemode) toggleMode(); nextSlideOrElt(); break; + case "++": if (!slidemode) toggleMode(); nextSlide(); break; + case "-": if (!slidemode) toggleMode(); previousSlideOrElt(); break; + case "--": if (!slidemode) toggleMode(); previousSlide(); break; + case "^": if (!slidemode) toggleMode(); firstSlide(); break; + case "$": if (!slidemode) toggleMode(); lastSlide(); break; + case "0": if (slidemode) toggleMode(); break; + case ":dark-on": toggleDarkMode("on"); break; + case ":dark-off": toggleDarkMode("off"); break; + default: if (!slidemode) toggleMode(); targetSlide(command); // ID or # + } + } else { // There is a 2nd window + secondwindow.postMessage({event: "sync", v: command}); + } +} + + +/* syncHandler -- handle a server-sent event with a slide number or slide ID */ +function syncHandler(event) +{ + if (syncmode) syncSlide(event.data); +} + + +/* fullscreenChanged -- handle a fullscreenchange event */ +function fullscreenChanged(ev) +{ + console.assert(!firstwindow); // We assume this is the first window + + if (switchFullscreen) { + // We are entering or leaving fullscreen mode because the F1 or F + // key was pressed. Reset the flag, but don't change slide mode. + switchFullscreen = false; + } else { + // We are entering or leaving fullscreen mode, but not because the + // F1 or F keys were pressed. (Most likely, the Escape key was + // pressed while in fullscreen mode, or the user used a browser + // function to enter fullscreen mode.) If we are leaving + // fullscreen mode while in slide mode, then leave slide mode. And + // if we are entering fullscreen mode while not in slide mode, + // then enter slide mode. + if ((! document.fullscreenElement && slidemode) || + (document.fullscreenElement && !slidemode)) { + // If the help text is on screen, remove it. Do this before + // calling toggleMode(), because toggleMode() changes the style + // attribute of all toplevel elements and we don't want the + // style of the helptext to change. + if (helptext?.parentElement) helptext.remove(); + toggleMode(); + } + } +} + + +/* playButtonClick -- handle activation of the play/stop button */ +function playButtonClick(ev) +{ + if (syncmode) warnSyncMode(); + else if (secondwindow?.closed !== false) toggleModeAndFullscreen(); + else secondwindow.postMessage({event: "keydown", v: "a"}); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* secondWindowButtonClick -- handle activation of the 2nd-window button */ +function secondWindowButtonClick(ev) +{ + console.assert(!firstwindow); // We're on the first window + + if (syncmode) warnSyncMode() + else if (secondwindow?.closed !== false) openSecondWindow() + else secondwindow.postMessage({event: "keydown", v: "a"}); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* prevButtonClick -- handle activation of the prevbutton */ +function prevButtonClick(ev) +{ + // We're on the first window. + console.assert(!firstwindow); + + if (secondwindow?.closed === false) { + // There is a second window, send a left arrow key key event to it. + secondwindow.postMessage({event: "keydown", v: "ArrowLeft"}); + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); + } else { + // Start slide mode, as if play was clicked + console.assert(!slidemode); + playButtonClick(ev); + } +} + + +/* nextButtonClick -- handle activation of the nextbutton */ +function nextButtonClick(ev) +{ + // We're on the first window. + console.assert(!firstwindow); + + if (secondwindow?.closed === false) { + // There is a second window, send a space bar key event to it. + secondwindow.postMessage({event: "keydown", v: " "}); + ev.currentTarget.blur(); + ev.stopPropagation(); + ev.preventDefault(); + } else { + // Start slide mode, as if play was clicked + console.assert(!slidemode); + playButtonClick(ev); + } +} + + +/* darkModeButtonClick -- handle activation of the darkmodebutton */ +function darkModeButtonClick(ev) +{ + if (syncmode) warnSyncMode() + else toggleDarkMode(); + + // Avoid capturing keyboard events that are meant for navigating slides: + ev.currentTarget.blur(); + + ev.stopPropagation(); + ev.preventDefault(); +} + + +/* beforeUnload -- handle a beforeunload event */ +function beforeUnload(ev) +{ + if (secondwindow?.closed === false) secondwindow.close(); +} + + +/* initDarkMode -- set hasDarkMode to true/false depending on the style sheet */ +function initDarkMode() +{ + var e; + + // A style sheet that supports the class "darkmode" on the body + // element, should signal that by setting the property + // "--has-darkmode" to "1" on elements with class "has-darkmode". So + // we create a temporary, invisible element with class + // "has-darkmode" and check if it has the property. + + e = document.createElement("span"); + e.style.setProperty("display", "none"); + e.classList.add("has-darkmode"); + document.body.append(e); + hasDarkMode = window.getComputedStyle(e).getPropertyValue("--has-darkmode") + == "1"; + e.remove(); +} + + +/* addUI -- add buttons for slide mode and help */ +function addUI() +{ + var playbutton, secondwindowbutton, helpbutton, prevbutton, nextbutton, + darkmodebutton, div; + + if (firstwindow) return; // Do not add buttons on a second window + + // Wrap the buttons in a div with class "b6-ui". + div = document.createElement("div"); + div.setAttribute("class", "b6-ui"); + + // Create a play button. Clicking it has the same effect as pressing + // the "A" key, i.e., enter slide mode. + playbutton = document.createElement("button"); + playbutton.innerHTML = "" + _("▶\uFE0E") + " " + + _("play/stop") + ""; + playbutton.setAttribute("class", "b6-playbutton"); + playbutton.setAttribute("title", _("play slides or stop playing")); + playbutton.addEventListener("click", playButtonClick); + playbutton.addEventListener("dblclick", ignoreEvent); + div.append(playbutton); + + // Create a 2nd-window button. Clicking it has the same effect as + // pressing "2" when in slide mode, i.e., create a second window. + secondwindowbutton = document.createElement("button"); + secondwindowbutton.innerHTML = "" + _("⧉") + " " + + _("play in 2nd window") + ""; + secondwindowbutton.setAttribute("class", "b6-secondwindowbutton"); + secondwindowbutton.setAttribute("title", _("play/stop slides in a 2nd window")); + secondwindowbutton.addEventListener("click", secondWindowButtonClick); + secondwindowbutton.addEventListener("dblclick", ignoreEvent); + div.append(secondwindowbutton); + + /* Create buttons for next and previous. */ + prevbutton = document.createElement("button"); + prevbutton.innerHTML = "" + _("❮") + " " + + _("back") + ""; + prevbutton.setAttribute("class", "b6-prevbutton"); + prevbutton.setAttribute("title", _("previous slide")) + prevbutton.addEventListener("click", prevButtonClick); + prevbutton.addEventListener("dblclick", ignoreEvent); + div.append(prevbutton); + + nextbutton = document.createElement("button"); + nextbutton.innerHTML = "" + _("❯") + " " + + _("forward") + ""; + nextbutton.setAttribute("class", "b6-nextbutton"); + nextbutton.setAttribute("title", _("next slide or element")) + nextbutton.addEventListener("click", nextButtonClick); + nextbutton.addEventListener("dblclick", ignoreEvent); + div.append(nextbutton); + + // Create a dark mode toggle, if the style sheet has support for + // dark mode. Clicking it has the same effect as pressing "d" when + // in slide mode, i.e., add or remove class=darkmode on BODY. + if (hasDarkMode) { + darkmodebutton = document.createElement("button"); + darkmodebutton.innerHTML = "" + _("◑") + " " + + _("dark mode") + ""; + darkmodebutton.setAttribute("class", "b6-darkmodebutton"); + darkmodebutton.setAttribute("title", _("toggle dark mode on/off")); + darkmodebutton.addEventListener("click", darkModeButtonClick); + darkmodebutton.addEventListener("dblclick", ignoreEvent); + div.append(darkmodebutton); + } + + // Create a help button. Clicking it has the same effect as pressing + // "?" when in slide mode, i.e., pop up the help window. + helpbutton = document.createElement("button"); + helpbutton.innerHTML = "" + _("?") + " " + _("help") + + ""; + helpbutton.setAttribute("class", "b6-helpbutton"); + helpbutton.setAttribute("title", _("help")); + helpbutton.addEventListener("click", ev => { + help(); + ev.currentTarget.blur(); + ev.preventDefault(); + ev.stopPropagation(); + }); + helpbutton.addEventListener("dblclick", ignoreEvent); + div.append(helpbutton); + + // // Add logo of b6+ with a link to its home page. + // div.insertAdjacentHTML("beforeend", + // "" + + // ""); + + // Insert the div before the slides. + document.body.prepend(div); +} + + +/* checkURL -- process query parameters ("full", "static" and "sync") */ +function checkURL() +{ + const params = new URLSearchParams(location.search); + if (params.get("full") != null) fullmode = true; + if (params.get("static") != null) interactive = false; + if ((syncURL = params.get("sync"))) tryToggleSync(); +} + + +/* checkIfFramed -- if we're inside an iframe, add target=_parent to links */ +function checkIfFramed() +{ + var anchors, i; + + if (window.parent != window) { // Only if we're not the top document + anchors = document.getElementsByTagName('a'); + for (i = 0; i < anchors.length; i++) + if (!anchors[i].hasAttribute('target')) + anchors[i].setAttribute('target', '_parent'); + document.body.classList.add('framed'); // Allow the style to do things + } +} + + +/* checkOptions -- look for b6plus options in the class attribute on body */ +function checkOptions() +{ + var c, t; + + for (c of document.body.classList) + if (c === 'noclick') + noclick = true; + else if ((t = c.match(/^hidemouse(=([0-9.]+))?$/))) + hideMouseTime = 1000 * (t[2] ?? 5); // Default is 5s if no time given + else if ((t = c.match(/^incremental-([a-z]+)$/))) + incrementalsBehavior = t[1]; + + if (incrementalsBehavior !== "freeze" && + incrementalsBehavior !== "reset" && + incrementalsBehavior !== "forwardonly" && + incrementalsBehavior !== "symmetric") { + console.warn(`"${incrementalsBehavior}" is not a valid value after "incremental=". Must be one of "symmetric", "reset", "forwardonly" or "freeze". Falling back to "freeze".`); + incrementalsBehavior = "freeze"; + } +} + + +/* initLanguage -- determine the language to localize to */ +function initLanguage() +{ + var i; + + // Get language from the HTML element, default to en-us + language = (document.documentElement.getAttribute("lang") ?? "en-us") + .toLowerCase(); + + // Remove subtags until we have a match among the translations of "min" + while (!translations["min"][language] && (i = language.lastIndexOf("-")) >= 0) + language = language.slice(0, i); +} + + +/* checkIfSecondWindow -- if this is a second windo, configure it */ +function checkIfSecondWindow() +{ + var styleElt; + + if (window.opener && window.opener != window) { + + // If this is a second window, remember the corresponding first + // one, so we can use postMessage() on it. We need to store it in + // a variable, because after the next open("#foo"), window.opener + // will be reset to window. + firstwindow = window.opener; + + // Modify the title. + document.title = "b6+ slide window – " + document.title; + + // Accessibility hint. + document.body.setAttribute("role", "application"); + + // Add a message handler for messages from the first window. + window.addEventListener("message", message); + + // Go into slide mode. Once in slide mode, finishToggleMode() will + // send an "init" event to the first window. + toggleMode(); + } +} + + +/* initialize -- add event handlers, initialize state */ +function initialize() +{ + initLanguage(); // Determine the language for localized text + checkIfFramed(); // Add target attributes if needed + checkURL(); // Parse query parameters (full, static) + checkOptions(); // Look for options in body.classList + checkIfSecondWindow(); // If this is a secondwindow, configure it + numberSlides(); // Count & number the slides and give them IDs + window.addEventListener('resize', windowResize, true); + + if (interactive) { // Only add event listeners if not static + initClocks(); // Find and initialize clock elements + initDarkMode(); // Set hasDarkMode to true or false + addUI(); // Add buttons for slide mode and help + if (!noclick) document.addEventListener('click', mouseButtonClick, false); + document.addEventListener('keydown', keyDown, true); + document.addEventListener('dblclick', doubleClick, false); + window.addEventListener('hashchange', hashchange, false); + document.addEventListener('touchstart', gestureStart, false); + document.addEventListener('touchmove', gestureMove, false); + document.addEventListener('touchend', gestureEnd, false); + document.addEventListener('touchcancel', gestureCancel, false); + window.addEventListener("message", message, false); + window.addEventListener("beforeunload", beforeUnload, false); + if (!firstwindow) + document.addEventListener("fullscreenchange", fullscreenChanged, false); + } + + if (fullmode) toggleMode(); // Slide mode, but not fullscreen +} + + +/* main */ +if (!!document.b6IsLoaded) return; // Don't load b6plus twice +document.b6IsLoaded = true; + +if (document.readyState !== 'loading') initialize(); +else document.addEventListener('DOMContentLoaded', initialize); + +})(); diff --git a/presentations/ac2024/Templates/banner-dark.webp b/presentations/ac2024/Templates/banner-dark.webp new file mode 100644 index 0000000..fa67c97 Binary files /dev/null and b/presentations/ac2024/Templates/banner-dark.webp differ diff --git a/presentations/ac2024/Templates/banner.webp b/presentations/ac2024/Templates/banner.webp new file mode 100644 index 0000000..293affc Binary files /dev/null and b/presentations/ac2024/Templates/banner.webp differ diff --git a/presentations/ac2024/Templates/iframe-fixup.js b/presentations/ac2024/Templates/iframe-fixup.js new file mode 100644 index 0000000..ddc2d51 --- /dev/null +++ b/presentations/ac2024/Templates/iframe-fixup.js @@ -0,0 +1,64 @@ +// This script checks if the document is displayed inside an iframe or +// similar and if so: +// +// * adds target=_parent to links (unless they already have a +// target attribute), so the links replace the parent instead of +// opening inside the iframe, and +// +// * adds class=framed to the body element, so that the style sheet +// can apply suitable styles, if needed. +// +// This is useful, e.g., in HTML slides that use the Shower script +// (and probably other scripts, too) to allow the slides to be +// displayed inside iframe elements in another document. Add the +// script to the slides with: +// +// +// +// and then include a slide in an iframe with, e.g.: +// +// +// +// The "?full" at the end of the URL tells Shower, b6+ and similar +// slide frameworks to display a single slide; and the "#cover" tells +// them to display the slide that has id=cover. +// +// This script is not necessary with the b6+ slide framework, which +// already includes equivalent code. (But it is not harmful either.) +// +// Created: 19 December 2020 +// Author: Bert Bos + + +(function() { + "use strict"; + + + // checkIfFramed -- apply some fixes if we are inside an iframe + function checkIfFramed() + { + var anchors, i; + + // Check that we're not the top document and not yet marked. + if (window.parent != window && + !document.body.classList.contains('framed')) { + + // Add target=_parent to all hyperlinks that do not have a target. + anchors = document.getElementsByTagName('a'); + for (i = 0; i < anchors.length; i++) + if (!anchors[i].hasAttribute('target')) + anchors[i].setAttribute('target', '_parent'); + + // Add a class to allow the style to do things. + document.body.classList.add('framed'); + } + } + + + // Do it if the document has been loaded, otherwise as soon as it has been. + if (document.readyState !== 'loading') checkIfFramed(); + else document.addEventListener('DOMContentLoaded', checkIfFramed); + +})(); diff --git a/presentations/ac2024/Templates/linen.png b/presentations/ac2024/Templates/linen.png new file mode 100644 index 0000000..46474bf Binary files /dev/null and b/presentations/ac2024/Templates/linen.png differ diff --git a/presentations/ac2024/Templates/shower.js b/presentations/ac2024/Templates/shower.js new file mode 100644 index 0000000..15d916b --- /dev/null +++ b/presentations/ac2024/Templates/shower.js @@ -0,0 +1,836 @@ +/** + * Core for Shower HTML presentation engine + * @shower/core v3.2.0, https://github.com/shower/core + * @copyright 2010–2021 Vadim Makeev, https://pepelsbey.net + * @license MIT + */ +(function () { + 'use strict'; + + const isInteractiveElement = (element) => element.tabIndex !== -1; + + const contentLoaded = (callback) => { + if (document.currentScript.async) { + callback(); + } else { + document.addEventListener('DOMContentLoaded', callback); + } + }; + + const defineReadOnly = (target, props) => { + for (const [key, value] of Object.entries(props)) { + Object.defineProperty(target, key, { + value, + writable: false, + enumerable: true, + configurable: true, + }); + } + }; + + class ShowerError extends Error {} + + var defaultOptions = { + containerSelector: '.shower', + progressSelector: '.progress', + stepSelector: '.next', + fullModeClass: 'full', + listModeClass: 'list', + mouseHiddenClass: 'pointless', + mouseInactivityTimeout: 5000, + + slideSelector: '.slide', + slideTitleSelector: 'h2', + activeSlideClass: 'active', + visitedSlideClass: 'visited', + }; + + class Slide extends EventTarget { + /** + * @param {Shower} shower + * @param {HTMLElement} element + */ + constructor(shower, element) { + super(); + + defineReadOnly(this, { + shower, + element, + state: { + visitCount: 0, + innerStepCount: 0, + }, + }); + + this._isActive = false; + this._options = this.shower.options; + + this.element.addEventListener('click', (event) => { + if (event.defaultPrevented) return; + + this.activate(); + this.shower.enterFullMode(); + }); + } + + get isActive() { + return this._isActive; + } + + get isVisited() { + return this.state.visitCount > 0; + } + + get id() { + return this.element.id; + } + + get title() { + const titleElement = this.element.querySelector(this._options.slideTitleSelector); + return titleElement ? titleElement.innerText : ''; + } + + /** + * Deactivates currently active slide (if any) and activates itself. + * @emits Slide#deactivate + * @emits Slide#activate + * @emits Shower#slidechange + */ + activate() { + if (this._isActive) return; + + const prev = this.shower.activeSlide; + if (prev) { + prev._deactivate(); + } + + this.state.visitCount++; + this.element.classList.add(this._options.activeSlideClass); + + this._isActive = true; + this.dispatchEvent(new Event('activate')); + this.shower.dispatchEvent( + new CustomEvent('slidechange', { + detail: { prev }, + }), + ); + } + + /** + * @throws {ShowerError} + * @emits Slide#deactivate + */ + deactivate() { + if (this.shower.isFullMode) { + throw new ShowerError('In full mode, another slide should be activated instead.'); + } + + if (this._isActive) { + this._deactivate(); + } + } + + _deactivate() { + this.element.classList.replace( + this._options.activeSlideClass, + this._options.visitedSlideClass, + ); + + this._isActive = false; + this.dispatchEvent(new Event('deactivate')); + } + } + + const createLiveRegion = () => { + const liveRegion = document.createElement('section'); + liveRegion.className = 'region'; + liveRegion.setAttribute('role', 'region'); + liveRegion.setAttribute('aria-live', 'assertive'); + liveRegion.setAttribute('aria-relevant', 'all'); + liveRegion.setAttribute('aria-label', 'Slide Content: Auto-updating'); + return liveRegion; + }; + + var a11y = (shower) => { + const { container } = shower; + const liveRegion = createLiveRegion(); + container.appendChild(liveRegion); + + const updateDocumentRole = () => { + if (shower.isFullMode) { + container.setAttribute('role', 'application'); + } else { + container.removeAttribute('role'); + } + }; + + const updateLiveRegion = () => { + const slide = shower.activeSlide; + if (slide) { + liveRegion.innerHTML = slide.element.innerHTML; + } + }; + + shower.addEventListener('start', () => { + updateDocumentRole(); + updateLiveRegion(); + }); + + shower.addEventListener('modechange', updateDocumentRole); + shower.addEventListener('slidechange', updateLiveRegion); + }; + + var keys = (shower) => { + const doSlideActions = (event) => { + const isShowerAction = !(event.ctrlKey || event.altKey || event.metaKey); + + switch (event.key.toUpperCase()) { + case 'ENTER': + if (event.metaKey && shower.isListMode) { + if (event.shiftKey) { + event.preventDefault(); + shower.first(); + } + + break; + } + + event.preventDefault(); + if (event.shiftKey) { + shower.prev(); + } else { + shower.next(); + } + break; + + case 'BACKSPACE': + case 'PAGEUP': + case 'ARROWUP': + case 'ARROWLEFT': + case 'H': + case 'K': + case 'P': + if (isShowerAction) { + event.preventDefault(); + shower.prev(event.shiftKey); + } + break; + + case 'PAGEDOWN': + case 'ARROWDOWN': + case 'ARROWRIGHT': + case 'L': + case 'J': + case 'N': + if (isShowerAction) { + event.preventDefault(); + shower.next(event.shiftKey); + } + break; + + case ' ': + if (isShowerAction && shower.isFullMode) { + event.preventDefault(); + if (event.shiftKey) { + shower.prev(); + } else { + shower.next(); + } + } + break; + + case 'HOME': + event.preventDefault(); + shower.first(); + break; + + case 'END': + event.preventDefault(); + shower.last(); + break; + } + }; + + const doModeActions = (event) => { + switch (event.key.toUpperCase()) { + case 'ESCAPE': + if (shower.isFullMode) { + event.preventDefault(); + shower.exitFullMode(); + } + break; + + case 'ENTER': + if (event.metaKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + + case 'P': + if (event.metaKey && event.altKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + + case 'F5': + if (event.shiftKey && shower.isListMode) { + event.preventDefault(); + shower.enterFullMode(); + } + break; + } + }; + + shower.container.addEventListener('keydown', (event) => { + if (event.defaultPrevented) return; + if (isInteractiveElement(event.target)) return; + + doSlideActions(event); + doModeActions(event); + }); + }; + + var location$1 = (shower) => { + const composeURL = () => { + const search = shower.isFullMode ? '?full' : ''; + const slide = shower.activeSlide; + const hash = slide ? `#${slide.id}` : ''; + + return location.pathname + search + hash; // path is required to clear search params + }; + + const applyURLMode = () => { + const isFull = new URLSearchParams(location.search).has('full'); + if (isFull) { + shower.enterFullMode(); + } else { + shower.exitFullMode(); + } + }; + + const applyURLSlide = () => { + const id = location.hash.slice(1); + if (!id) return; + + const target = shower.slides.find((slide) => slide.id === id); + if (target) { + target.activate(); + } else if (!shower.activeSlide) { + shower.first(); // invalid hash + } + }; + + const applyURL = () => { + applyURLMode(); + applyURLSlide(); + }; + + applyURL(); + window.addEventListener('popstate', applyURL); + + shower.addEventListener('start', () => { + history.replaceState(null, document.title, composeURL()); + }); + + shower.addEventListener('modechange', () => { + history.replaceState(null, document.title, composeURL()); + }); + + shower.addEventListener('slidechange', () => { + const url = composeURL(); + if (!location.href.endsWith(url)) { + history.pushState(null, document.title, url); + } + }); + }; + + var next = (shower) => { + const { stepSelector, activeSlideClass, visitedSlideClass } = shower.options; + + let innerSteps; + let activeIndex; + + const isActive = (step) => step.classList.contains(activeSlideClass); + const isVisited = (step) => step.classList.contains(visitedSlideClass); + + const setInnerStepsState = () => { + if (shower.isListMode) return; + + const slide = shower.activeSlide; + + innerSteps = [...slide.element.querySelectorAll(stepSelector)]; + activeIndex = + innerSteps.length && innerSteps.every(isVisited) + ? innerSteps.length + : innerSteps.filter(isActive).length - 1; + + slide.state.innerStepCount = innerSteps.length; + }; + + shower.addEventListener('start', setInnerStepsState); + shower.addEventListener('modechange', setInnerStepsState); + shower.addEventListener('slidechange', setInnerStepsState); + + shower.addEventListener('next', (event) => { + if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; + + activeIndex++; + innerSteps.forEach((step, index) => { + step.classList.toggle(visitedSlideClass, index < activeIndex); + step.classList.toggle(activeSlideClass, index === activeIndex); + }); + + if (activeIndex < innerSteps.length) { + event.preventDefault(); + } + }); + + shower.addEventListener('prev', (event) => { + if (shower.isListMode || event.defaultPrevented || !event.cancelable) return; + if (activeIndex === -1 || activeIndex === innerSteps.length) return; + + activeIndex--; + innerSteps.forEach((step, index) => { + step.classList.toggle(visitedSlideClass, index < activeIndex + 1); + step.classList.toggle(activeSlideClass, index === activeIndex); + }); + + event.preventDefault(); + }); + }; + + var progress = (shower) => { + const { progressSelector } = shower.options; + const bar = shower.container.querySelector(progressSelector); + if (!bar) return; + + bar.setAttribute('role', 'progressbar'); + bar.setAttribute('aria-valuemin', 0); + bar.setAttribute('aria-valuemax', 100); + + const updateProgress = () => { + const index = shower.activeSlideIndex; + const { length } = shower.slides; + const progress = (index / (length - 1)) * 100; + + bar.style.width = `${progress}%`; + bar.setAttribute('aria-valuenow', progress); + bar.setAttribute('aria-valuetext', `Slideshow progress: ${progress}%`); + }; + + shower.addEventListener('start', updateProgress); + shower.addEventListener('slidechange', updateProgress); + }; + + const units = ['s', 'm', 'h']; + const hasUnits = (timing) => { + return units.some((unit) => timing.includes(unit)); + }; + + const parseUnits = (timing) => { + return units.map((unit) => timing.match(`(\\S+)${unit}`)).map((match) => match && match[1]); + }; + + const parseColons = (timing) => { + return `::${timing}`.split(':').reverse(); + }; + + const SEC_IN_MIN = 60; + const SEC_IN_HOUR = SEC_IN_MIN * 60; + + var parseTiming = (timing) => { + if (!timing) return 0; + + const parsed = hasUnits(timing) ? parseUnits(timing) : parseColons(timing); + + let [sec, min, hour] = parsed.map(Number); + + sec += min * SEC_IN_MIN; + sec += hour * SEC_IN_HOUR; + + return Math.max(sec * 1000, 0); + }; + + var timer = (shower) => { + let id; + + const resetTimer = () => { + clearTimeout(id); + if (shower.isListMode) return; + + const slide = shower.activeSlide; + const { visitCount, innerStepCount } = slide.state; + if (visitCount > 1) return; + + const timing = parseTiming(slide.element.dataset.timing); + if (!timing) return; + + if (innerStepCount) { + const stepTiming = timing / (innerStepCount + 1); + id = setInterval(() => shower.next(), stepTiming); + } else { + id = setTimeout(() => shower.next(), timing); + } + }; + + shower.addEventListener('start', resetTimer); + shower.addEventListener('modechange', resetTimer); + shower.addEventListener('slidechange', resetTimer); + + shower.container.addEventListener('keydown', (event) => { + if (!event.defaultPrevented) { + clearTimeout(id); + } + }); + }; + + const mdash = '\u2014'; + + var title = (shower) => { + const { title } = document; + const updateTitle = () => { + if (shower.isFullMode) { + const slide = shower.activeSlide; + const slideTitle = slide.title; + if (slideTitle) { + document.title = `${slideTitle} ${mdash} ${title}`; + return; + } + } + + document.title = title; + }; + + shower.addEventListener('start', updateTitle); + shower.addEventListener('modechange', updateTitle); + shower.addEventListener('slidechange', updateTitle); + }; + + var view = (shower) => { + const { container } = shower; + const { fullModeClass, listModeClass } = shower.options; + + if (container.classList.contains(fullModeClass)) { + shower.enterFullMode(); + } else { + container.classList.add(listModeClass); + } + + const updateScale = () => { + const firstSlide = shower.slides[0]; + if (!firstSlide) return; + + const { innerWidth, innerHeight } = window; + const { offsetWidth, offsetHeight } = firstSlide.element; + + const listScale = 1 / (offsetWidth / innerWidth); + const fullScale = 1 / Math.max(offsetWidth / innerWidth, offsetHeight / innerHeight); + + container.style.setProperty('--shower-list-scale', listScale); + container.style.setProperty('--shower-full-scale', fullScale); + }; + + const updateModeView = () => { + if (shower.isFullMode) { + container.classList.remove(listModeClass); + container.classList.add(fullModeClass); + } else { + container.classList.remove(fullModeClass); + container.classList.add(listModeClass); + } + + updateScale(); + + if (shower.isFullMode) return; + + const slide = shower.activeSlide; + if (slide) { + slide.element.scrollIntoView({ block: 'center' }); + } + }; + + shower.addEventListener('start', updateModeView); + shower.addEventListener('modechange', updateModeView); + shower.addEventListener('slidechange', () => { + if (shower.isFullMode) return; + + const slide = shower.activeSlide; + slide.element.scrollIntoView({ block: 'nearest' }); + }); + + window.addEventListener('resize', updateScale); + }; + + var touch = (shower) => { + let exitFullScreen = false; + let clickable = false; + + document.addEventListener('touchstart', (event) => { + if (event.touches.length === 1) { + const touch = event.touches[0]; + const x = touch.clientX; + const { target } = touch; + clickable = target.tabIndex !== -1; + if (!clickable) { + if (shower.isFullMode) { + if (event.cancelable) event.preventDefault(); + if (window.innerWidth / 2 < x) { + shower.next(); + } else { + shower.prev(); + } + } + } + } else if (event.touches.length === 3) { + exitFullScreen = true; + } + }); + + shower.container.addEventListener('touchend', (event) => { + if (exitFullScreen) { + event.preventDefault(); + exitFullScreen = false; + shower.exitFullMode(); + } else if (event.touches.length === 1 && !clickable && shower.isFullMode) + event.preventDefault(); + }); + }; + + var mouse = (shower) => { + const { mouseHiddenClass, mouseInactivityTimeout } = shower.options; + + let hideMouseTimeoutId = null; + + const cleanUp = () => { + shower.container.classList.remove(mouseHiddenClass); + clearTimeout(hideMouseTimeoutId); + hideMouseTimeoutId = null; + }; + + const hideMouseIfInactive = () => { + if (hideMouseTimeoutId !== null) { + cleanUp(); + } + + hideMouseTimeoutId = setTimeout(() => { + shower.container.classList.add(mouseHiddenClass); + }, mouseInactivityTimeout); + }; + + const initHideMouseIfInactiveModule = () => { + shower.container.addEventListener('mousemove', hideMouseIfInactive); + }; + + const destroyHideMouseIfInactiveModule = () => { + shower.container.removeEventListener('mousemove', hideMouseIfInactive); + cleanUp(); + }; + + const handleModeChange = () => { + if (shower.isFullMode) { + initHideMouseIfInactiveModule(); + } else { + destroyHideMouseIfInactiveModule(); + } + }; + + shower.addEventListener('start', handleModeChange); + shower.addEventListener('modechange', handleModeChange); + }; + + var installModules = (shower) => { + a11y(shower); + progress(shower); + keys(shower); + next(shower); + timer(shower); // should come after `keys` and `next` + title(shower); + location$1(shower); // should come after `title` + view(shower); + touch(shower); + mouse(shower); + + // maintains invariant: active slide always exists in `full` mode + if (shower.isFullMode && !shower.activeSlide) { + shower.first(); + } + }; + + class Shower extends EventTarget { + /** + * @param {object=} options + */ + constructor(options) { + super(); + + defineReadOnly(this, { + options: { ...defaultOptions, ...options }, + }); + + this._mode = 'list'; + this._isStarted = false; + this._container = null; + } + + /** + * @param {object=} options + * @throws {ShowerError} + */ + configure(options) { + if (this._isStarted) { + throw new ShowerError('Shower should be configured before it is started.'); + } + + Object.assign(this.options, options); + } + + /** + * @throws {ShowerError} + * @emits Shower#start + */ + start() { + if (this._isStarted) return; + + const { containerSelector } = this.options; + this._container = document.querySelector(containerSelector); + if (!this._container) { + throw new ShowerError( + `Shower container with selector '${containerSelector}' was not found.`, + ); + } + + this._initSlides(); + installModules(this); + + this._isStarted = true; + this.dispatchEvent(new Event('start')); + } + + _initSlides() { + const visibleSlideSelector = `${this.options.slideSelector}:not([hidden])`; + const visibleSlideElements = this._container.querySelectorAll(visibleSlideSelector); + + this.slides = Array.from(visibleSlideElements, (slideElement, index) => { + if (!slideElement.id) { + slideElement.id = index + 1; + } + + return new Slide(this, slideElement); + }); + } + + _setMode(mode) { + if (mode === this._mode) return; + + this._mode = mode; + this.dispatchEvent(new Event('modechange')); + } + + /** + * @param {Event} event + */ + dispatchEvent(event) { + if (!this._isStarted) return false; + + return super.dispatchEvent(event); + } + + get container() { + return this._container; + } + + get isFullMode() { + return this._mode === 'full'; + } + + get isListMode() { + return this._mode === 'list'; + } + + get activeSlide() { + return this.slides.find((slide) => slide.isActive); + } + + get activeSlideIndex() { + return this.slides.findIndex((slide) => slide.isActive); + } + + /** + * Slide fills the maximum area. + * @emits Shower#modechange + */ + enterFullMode() { + this._setMode('full'); + } + + /** + * Shower returns into list mode. + * @emits Shower#modechange + */ + exitFullMode() { + this._setMode('list'); + } + + /** + * @param {number} index + */ + goTo(index) { + const slide = this.slides[index]; + if (slide) { + slide.activate(); + } + } + + /** + * @param {number} delta + */ + goBy(delta) { + this.goTo(this.activeSlideIndex + delta); + } + + /** + * @param {boolean} [isForce=false] + * @emits Shower#prev + */ + prev(isForce) { + const prev = new Event('prev', { cancelable: !isForce }); + if (this.dispatchEvent(prev)) { + this.goBy(-1); + } + } + + /** + * @param {boolean} [isForce=false] + * @emits Shower#next + */ + next(isForce) { + const next = new Event('next', { cancelable: !isForce }); + if (this.dispatchEvent(next)) { + this.goBy(1); + } + } + + first() { + this.goTo(0); + } + + last() { + this.goTo(this.slides.length - 1); + } + } + + const options = document.currentScript.dataset; + const shower = new Shower(options); + + Object.defineProperty(window, 'shower', { + value: shower, + configurable: true, + }); + + contentLoaded(() => { + shower.start(); + }); + +})(); diff --git a/presentations/ac2024/Templates/side-banner-dark.webp b/presentations/ac2024/Templates/side-banner-dark.webp new file mode 100644 index 0000000..00727e4 Binary files /dev/null and b/presentations/ac2024/Templates/side-banner-dark.webp differ diff --git a/presentations/ac2024/Templates/side-banner.webp b/presentations/ac2024/Templates/side-banner.webp new file mode 100644 index 0000000..f1fe492 Binary files /dev/null and b/presentations/ac2024/Templates/side-banner.webp differ diff --git a/presentations/ac2024/Templates/slides.css b/presentations/ac2024/Templates/slides.css new file mode 100644 index 0000000..bd41d53 --- /dev/null +++ b/presentations/ac2024/Templates/slides.css @@ -0,0 +1,1016 @@ +/* Style for the slides for AC 2024, to be used together with the + Shower script or the b6+ script. For usage instructions, see + https://www.w3.org/2024/Talks/ac-slides/Templates/Overview.html + + TODO: Styles for blockquotes? + + TODO: Provide a fallback for side images for UAs that do not + implement 'object-fit'? + + TODO: .greeked is visually hidden, but assistive technology still + sees it and speaks it. Can that be fixed? ('speak: never' has no + effect.) + + Layout of a slide: + + +---------------------------------------+-------+ + | 2em | LOGO | + | +-------------------------------+ | | + | | | | | ^ + | | | | | | + |2em| |1em| 3em | 23em + | | | | | | + | | | | | v + | +-------------------------------+ | | + | 1em | nr | + +---------------------------------------+-------+ + + A = 16/9 = aspect ratio + N = 23 = height in em (i.e., 21 lines + 2 x 1 em padding) + L = 4 = logo width in em + H = 86/120.31532 = logo aspect ratio (width/height) + w = N*A = width of slide in em + + Created: 11 December 2023 (based on + https://www.w3.org/Talks/Tools/Shower3-2/humaaans.css) + + Author: Bert Bos + + Copyright © 2024 World Wide Web Consortium (W3C Inc, European + Research Consortium for Informatics and Mathematics, Keio + University, Beihang). All Rights Reserved. This work is distributed + under the W3C® Software License[1] in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied warranty + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + [1] http://www.w3.org/Consortium/Legal/copyright-software +*/ + +@font-face { + font-family: My Lato; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(Lato-Italic.woff2) format("woff2"), + url(Lato-Italic.woff) format("woff"); + src: local(Lato Italic), local(Lato-Italic), + url(Lato-Italic.woff2) format("woff2"), + url(Lato-Italic.woff) format("woff")} + +@font-face { + font-family: My Lato; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(Lato-Regular.woff2) format("woff2"), + url(Lato-Regular.woff) format("woff"); + src: local(Lato Regular), local(Lato-Regular), + url(Lato-Regular.woff2) format("woff2"), + url(Lato-Regular.woff) format("woff")} + +@font-face { + font-family: My Lato; + font-style: normal; + font-weight: bold; + font-display: swap; + src: url(Lato-Bold.woff2) format("woff2"), + url(Lato-Bold.woff) format("woff"); + src: local(Lato Bold), local(Lato-Bold), + url(Lato-Bold.woff2) format("woff2"), + url(Lato-Bold.woff) format("woff"); +} +@font-face { + font-family: My Lato; + font-style: italic; + font-weight: bold; + font-display: swap; + src: url(Lato-BoldItalic.woff2) format("woff2"), + url(Lato-BoldItalic.woff) format("woff"); + src: local(Lato Bold Italic), local(Lato-BoldItalic), + url(Lato-BoldItalic.woff2) format("woff2"), + url(Lato-BoldItalic.woff) format("woff"); +} +@font-face { + font-family: My Montserrat; + font-style: italic; + font-weight: 900; + font-display: swap; + src: url(Montserrat-BlackItalic.woff2) format("woff2"), + url(Montserrat-BlackItalic.woff) format("woff"); + src: local(Montserrat Black Italic), local(Montserrat-BlackItalic), + url(Montserrat-BlackItalic.woff2) format("woff2"), + url(Montserrat-BlackItalic.woff) format("woff"); +} +@font-face { + font-family: My Montserrat; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url(Montserrat-Black.woff2) format("woff2"), + url(Montserrat-Black.woff) format("woff"); + src: local(Montserrat Black), local(Montserrat-Black), + url(Montserrat-Black.woff2) format("woff2"), + url(Montserrat-Black.woff) format("woff"); +} + +@font-face { + font-family: My Montserrat; + font-style: italic; + font-weight: bold; + font-display: swap; + src: url(Montserrat-BoldItalic.woff2) format("woff2"), + url(Montserrat-BoldItalic.woff) format("woff"); + src: local(Montserrat BoldItalic), local(Montserrat-BoldItalic), + url(Montserrat-BoldItalic.woff2) format("woff2"), + url(Montserrat-BoldItalic.woff) format("woff"); +} + +@font-face { + font-family: My Montserrat; + font-style: normal; + font-weight: bold; + font-display: swap; + src: url(Montserrat-Bold.woff2) format("woff2"), + url(Montserrat-Bold.woff) format("woff"); + src: local(Montserrat Bold), local(Montserrat-Bold), + url(Montserrat-Bold.woff2) format("woff2"), + url(Montserrat-Bold.woff) format("woff"); +} + + +/* Common layout independent of slide mode */ +html {font: 400 1em/1.3 My Lato, Carlito, Calibri, Open Sans, Helvetica Neue, + Helvetica, Arial, Liberation Sans, Noto Emoji, Noto Sans Symbols, + sans-serif; + color-scheme: only light dark; /* only = disable Chromium's heuristics. */ + background: none; /* Make sure the background of body gets used */ + font-size-adjust: 0.506 /* Lato Regular */; letter-spacing: 0.02em} +body {background: url(linen.png) #595b60; counter-reset: slide; + margin: 2em 2em 9em; color: white} +b {font-weight: bold} +dt {font-weight: bold} +dd {margin: 0} +h4 {font-size: 1.2em; margin: 0.5em 0} +.slide p, .slide ul, .slide ol, .slide pre, .slide blockquote, .slide li { + margin: 0 0 0.6em 0} +.slide h1, .slide h2, .slide address {margin: 0 0 0.6em 0; + font: 900 2em/1.1 My Montserrat, Arial Black, Myriad Pro, Roboto, sans-serif; + font-size-adjust: 0.542} +.slide address {color: hsl(356,67%,40%); font-size: 1.4em} +.slide address :link, .slide address :visited {color: inherit} +.slide h3 {font-size: 1.1em; color: hsl(356,67%,40%); + margin: 0.8em 0 0.48em 0} +.full, .slide, .comment {width: 40.889em; /*= w */ height: 23em; /*= N */} +.slide {color: black; box-shadow: 0 0.4em 0.6em #000; + line-height: 1.6; + word-break: normal; overflow-wrap: normal; letter-spacing: normal; + padding: 2em 5em /*= L + 1 */ 1em 2em; + position: relative; + box-sizing: border-box; z-index: 0; display: inline-block; + margin: 4em 2em 0 0; vertical-align: bottom; counter-increment: slide; + border-radius: 0.5em; + text-shadow: 0 0 1px hsl(0,0%,98%), 0 0 1px hsl(0,0%,98%), + 0 0 1px hsl(0,0%,98%), 0 0 1px hsl(0,0%,98%); + background: top right / auto 100% url(side-banner.webp) no-repeat, + hsl(0,0%,98%)} +.slide:target {outline: lime solid 0.5em; outline-offset: 1em} +.slide h3 a {color: inherit} +.watermark {color: red; font-size: 400%} + +/* EM elements get a highlighter-like background */ +.slide em {font-style: normal; padding-left: 0.1em; padding-right: 0.1em; + text-shadow: none; background: hsl(62,100%,50%)} + +/* Lists with less indent */ +.slide li {margin-left: 1em} +.slide ol, .slide ul {padding: 0} +.slide li ul, .slide li ol, .slide li li {margin-top: 0.1em; margin-bottom: 0.2em} + +/* Own counter, because FF & Safari don't apply text-shadow to the default. */ +.slide ol {counter-reset: ol; list-style: none} +.slide ol > li {counter-increment: ol} +.slide ol > li::before {/*float: left;*/ display: inline-block; width: 2em; + margin-left: -2em; text-align: right; content: counter(ol) ".\A0"} +.slide ol > li > p:first-child {display: inline} +.slide ol[start="2"] {counter-reset: ol 1} +.slide ol[start="3"] {counter-reset: ol 2} +.slide ol[start="4"] {counter-reset: ol 3} +.slide ol[start="5"] {counter-reset: ol 4} +.slide ol[start="6"] {counter-reset: ol 5} +.slide ol[start="7"] {counter-reset: ol 6} +.slide ol[start] {counter-reset: ol calc(attr(start integer) - 1)} + +/* List with icons instead of bullets. */ +ul.with-icons > li {margin-left: 1.5em; list-style: none} +ul.with-icons > li > *:first-child {display: inline-block; + margin: 0 0.5em 0 -1.5em; width: 1em} + +/* Slides with an image on the left (.side) or right (.side.right) one third */ +.slide.side {padding-left: 12.867em /*= 2 + (w - L - 4) * 30% + 1 */} +.slide.side.right, .slide.side.r {padding-left: 2em; + padding-right: 15.867em; /*= L + 1 + (w - L - 4) * 30% + 1 */} +.side .side {position: absolute; top: 3em /* top margin + a bit */; left: 2em; + height: 19em /*= N - 4 */; object-fit: contain; + width: 9.8667em /*= (w - L - 4) * 30% */} +.side .side.cover {object-fit: cover; top: 0; left: 0; height: 23em /*= N */; + width: 11.867em /*= (w - L - 4) *30% + 2 */; border-radius: 0.5rem 0 0 0.5rem} +.side.right .side, .side.r .side { + left: 26.022em /*= w - L - 1 - (w - L - 4) * 30% */} +.side.right .side.cover, .side.r .side.cover {border-radius: 0 0.5rem 0.5rem 0; + width: 14.867em /*= L + 1 + (w - L - 4) * 30% */} + +/* Slides with a big, square image on the left or right */ +.slide.side.big {padding-left: 24em /*= N + 1 */} +.slide.side.right.big, .slide.side.r.big {padding-left: 2em; + padding-right: 24em /*= N + 1 */} +.side.big .side {top: 2em; left: 2em; + height: 19em /*= N - 4 */; width: 21em /*= N - 2 */} +.side.big.right .side, .side.big.r .side {left: 17.889em /*= w - N */} +.side.big .side.cover {object-fit: cover; top: 0; left: 0; + height: 23em /*= N */; width: 23em /*= N */} +.side.big.right .side.cover, .side.big.r .side.cover { + left: 17.889em /*= w - N */} + +/* Cover pages */ +.slide.cover {background: 100% 0 / auto 10em url(banner.webp) no-repeat, + 0% 0% / 100% 10em linear-gradient(#e7f1fa, #e7f1fa) no-repeat, + 100% 22em /*= N - 1 */ / auto 10em url(banner.webp) no-repeat, + 0% 22em /*=N - 1*/ / 100% 10em linear-gradient(#e7f1fa, #e7f1fa) no-repeat, + hsl(0,0%,98%); + padding: 2em 24.533em /*= 0.6 * w */ 13em /*= N - 10 */ 2em} +.slide.cover h1 {position: absolute; bottom: 3.2em; left: 1em; right: 1em; + margin: 0; text-align: center} +.slide.cover address {position: absolute; top: 12.21em; left: 1.43em; + right: 1.43em; margin: 0; text-align: center} +.slide.cover p {line-height: 1.2; color: #6b6b6b; text-align: right; + text-shadow: none} +.slide.cover p::first-line {font-size: 2em} +.slide.cover p img {float: left; margin: 0 1em 0 0; + width: 4em; height: 6em; object-fit: contain} + +/* Last page */ +.slide.final {background: 100% 18em /*= N - 5 */ / auto 5em url(banner.webp) + no-repeat, 0% 18em /*= N - 5 */ / 100% 5em + linear-gradient(#e7f1fa, #e7f1fa) no-repeat, hsl(0,0%,98%); + padding: 2em 2em 5.5em} + +/* Notes in a smaller font */ +.slide .note {font-size: 70%} + +/* Miscellaneous styles */ +.num {font-variant-numeric: oldstyle-nums tabular-nums diagonal-fractions} +.slide code, .slide pre {font-family: Andale Mono, Courier, monospace; + text-shadow: none} +.slide code {background: #eee; padding: 0.1em 0.3em; border-radius: 0.3em} +sub, sup {line-height: 0.5} +.slide pre {padding: 0 0.2em; background: black; color: hsl(120,100%,70%); + text-shadow: none} + +/* Explicit placement on a 3x3 grid */ +.place {position: absolute; box-sizing: border-box; + max-width: 25.996%; /*= (w - L - 5) / 3 / w */ + top: 50%; left: 46.332%; /*= (2 + (w - L - 2 - 1)/2) / w */ + transform: translate(-50%, -50%); text-align: center} +.place.t, .place.top {top: 8.6957%; /*= 2/N */ transform: translate(-50%,0)} +.place.b, .place.bottom {top: auto; bottom: 8.6957% /*= 2/N */; + transform: translate(-50%,0)} +.place.l, .place.left {left: 4.8913%; /*= 2 / w */ + transform: translate(0,-50%); text-align: left} +.place.r, .place.right {left: auto; right: 12.228%; /*= (L + 1)/w */ + transform: translate(0,-50%); text-align: right} +.place.t.l, .place.top.left, .place.t.r, .place.top.right, .place.b.l, +.place.bottom.left, .place.b.r, .place.bottom.right {transform: none} + +/* Numbered lines in a PRE */ +pre.numbered {padding-left: 2em; overflow-y: hidden; position: relative} +pre.numbered::before {color: #aaa; text-align: right; white-space: pre-line; + text-shadow: none; + content: "1\A 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20"; + position: absolute; top: 0; left: 0; width: 1.2em; font-family: serif; + border-right: thin solid; padding-right: 0.2em} + +/* Full-size image overlays */ +img.cover, img.fit {position: absolute; z-index: -1; top: 0; left: 0; + border-radius: 0.5em; + width: 100%; height: 100%; object-fit: cover; padding: 0} +img.fit {object-fit: contain} + +/* Slide number in upper right corner, in a white circle. */ +.slide::after {content: counter(slide); color: black; position: absolute; + right: 0; width: 4em /*= L */; top: 0; bottom: 0; padding: 2em 0; + text-shadow: none; + text-align: center; font: bold 1em/1 My Montserrat, sans-serif; + background: 50% 1.71em / auto 1.55em no-repeat + url(data:image/svg+xml,)} +.slide.cover::after {} +.slide.final::after {color: white; + background: 50% 1.71em / auto 1.55em no-repeat + url(data:image/svg+xml,)} +.clear .slide::after, .slide.clear::after {content: none} + +/* Two columns, and alternate elements in the left and right column */ +.slide .columns > * {box-sizing: border-box; + width: 47.134% /*= (w - L - 4)/2/(w - L - 2) */; float: right} +.slide .columns > *:nth-child(odd) {clear: both; float: left} +.slide .columns {overflow: hidden; line-height: 1.5 /* Reduced from 1.6 */} +.slide .columns > * > *:first-child {margin-top: 0} +@supports (display: grid) { + .slide .columns {overflow: visible; display: grid; grid: "a b" / 1fr 1fr; + grid-gap: 0.8em 2em; justify-items: normal} + .slide .columns > * {width: auto} +} +@supports not (display: grid) { + /* If grid is not supported and the column is a list, remove the margin */ + .slide .columns > li {margin-left: 0; list-style-position: inside} + .slide .columns > *:nth-child(n+3) {margin-top: 0.8em} /* gap between rows */ +} + +/* A list as a tree structure with box-drawing characters */ +.tree {list-style: none; font: 1em/1 monospace; + white-space: nowrap; padding: 0.2em 0; overflow: auto} +.tree li::before {content: none} +.tree code {background: none; padding: 0; + font-family: My Lato, Carlito, Calibri, Open Sans, + Helvetica Neue, Helvetica, Arial, Liberation Sans, sans-serif} +.comment .tree code {font-family: serif} + +/* A trick that may be useful for people who insist on putting a lot + of text on a slide: class "compact" can + be set on a list or other container and removes the top and bottom + margin from list items and paragraphs inside that container. */ +.slide .compact li, .slide .compact p {margin-top: 0; margin-bottom: 0} + +/* Striped tables */ +table.striped {border-collapse: collapse; margin-bottom:0.48em; width: 100%} +table.striped td, table.striped th {padding: 0.15em 0.3em; font-size: 0.93em; text-align: left} +table.striped tr:nth-child(2n+2) {background: #EEE; text-shadow: none} + +/* Takahashi method (very big text, very few words) */ +.shout {font-size: 400%; line-height: 1.1} +p.shout {margin: 0.25em 0} + +/* Figures, and images with collapsed descriptions */ +img {max-width: 100%;} +figure {text-align: center; margin: 0 0 0.6em 0} +figure img:not(.cover):not(.fit), summary img {display: block; + margin: 0 auto 0.6em auto; + max-height: 15.6em /*= N - 2 - 1.1 * 2 - 0.6 * 2 - 2 */} +.slide summary {list-style: none} /* Hide the triangle */ +.slide summary::-webkit-details-marker {display: none} /* Ditto webkit/blink */ +.slide [open] summary img {max-height: 4em} +.slide summary {outline: none} +.slide summary::before {content: "⊖"; float: left; width: 0.9em; + margin-left: -1.1em; text-align: left; line-height: 0.9} +.slide [open] > summary::before {content: "⊕"} +.slide summary:focus::before {outline: thin solid blue; + outline: thin solid invert} + +/* Keyboard keys */ +kbd {font-weight: bold; speak-as: spell-out} + +/* The progress element is normally empty */ +.progress {display: inline} + +/* Notes between the slides */ +.comment {background: black; color: white; padding: 1em; + font-family: Times New Roman, Times, serif; box-sizing: border-box; + display: inline-block; border-radius: 0.5em; margin: 4em 2em 0 0; + box-shadow: 0 0.4em 0.6em #000; vertical-align: bottom; overflow: auto} +.comment :link, .comment :visited {color: inherit; text-decoration: underline} +.comment pre {margin-left: 1em; font-family: Helvetica, sans-serif} +.comment :first-child {margin-top: 0} +.comment dd, .comment ul, .comment ol {padding-left: 1em; margin-left: 0} +.comment dd {margin-bottom: 1em} +.comment h1, .comment h2, .comment h3, .comment h4, .comment h5, .comment h6 { + break-after: avoid} +.slide ~ .comment:before {content: "notes for slide " counter(slide); + display: block; + text-align: center; font-size: small; font-variant: small-caps; + border-bottom: thin solid; padding-bottom: 0.3em; margin-bottom: 1em} + +/* Long comments */ +.comment.long {/*columns: 25em; column-rule: thin solid; column-gap: 2em; + widows: 2; orphans: 2; width: auto;*/ height: auto; display: block; + border-radius: 0; overflow: auto; + background: white; color: black} +.comment.long:before {content: none} + +/* Layout in slide mode (when body has class=full) */ +.full {transform: scale(var(--shower-full-scale))} /* For Shower 3.1/3.2 */ +.full, .full .slide {position: absolute; overflow: hidden} +.full {top: 50%; left: 50%; background: black; + margin: -11.5em /*= -N/2 */ 0 0 -20.444em /*= -w/2 */} +.full .slide {visibility: hidden; top: 0; left: 0; margin: 0} +.full .slide.active {visibility: visible} +.full .comment {display: none} +.full .slide:target {outline: none} + +.full .progress {position: absolute; top: 0; left: 0; height: 1px; + background: linear-gradient(to right, hsla(0,100%,50%,0),hsla(0,100%,50%,1)); + z-index: 1} +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .progress {transition: 0.5s} +} + +.full .watermark {position: absolute; top: 50%; left: 50%; z-index: 1; + margin: 0; transform: translate(-50%, -50%) rotate(-29deg)} + +/* Incremental display with elements replacing each other. In index + mode, the elements are side by side with a scroll bar to reach them + (and scroll snap to make scrolling easier). In slide mode, all + items are in the first slot, but at most one of them is visible. */ +.incremental.in-place, .overlay.in-place {display: grid; grid: "a" / 100%; + gap: 2em; grid-auto-columns: 100%; grid-auto-flow: column; + overflow: auto; scrollbar-width: thin; scroll-snap-type: x mandatory} +.incremental.in-place > *, .overlay.in-place > * {scroll-snap-align: end} +.full .incremental.in-place > *, .full .overlay.in-place > * {grid-area: a} +.full .incremental.in-place > .visited:not(.active):not(:last-child), +.full .overlay.in-place > .visited:not(.active):not(:last-child) { + visibility: hidden} + +/* Reveal elements one by one. (incremental/overlay only works with b6+) */ +.full .incremental > :not(.active):not(.visited), +.full .overlay > :not(.active):not(.visited), +.full .next:not(.active):not(.visited) {visibility: hidden} + +/* With class=greeked, elements aren't hidden, but shown as gray bars */ +.full .incremental > .greeked:not(.active):not(.visited), +.full .incremental.greeked > :not(.active):not(.visited), +.full .greeked .incremental > :not(.active):not(.visited), +.full.greeked .incremental > :not(.active):not(.visited), +.full .overlay > .greeked:not(.active):not(.visited), +.full .overlay.greeked > :not(.active):not(.visited), +.full .greeked .overlay > :not(.active):not(.visited), +.full.greeked .overlay > :not(.active):not(.visited), +.full .next.greeked:not(.active):not(.visited), +.full .greeked .next:not(.active):not(.visited), +.full.greeked .next:not(.active):not(.visited) {visibility: inherit; + text-shadow: none; background: hsl(0,0%,50%); color: transparent; + speak: never} + +/* With class=strong, the currently active element is red. */ +.full .incremental .active.strong, .full .overlay .active.strong, +.full .incremental.strong .active, .full .overlay.strong .active, +.full .strong .incremental .active, .full .strong .overlay .active, +.full.strong .incremental .active, .full.strong .overlay .active, +.full .strong .next.active, .full .next.active.strong, +.full.strong .next.active {color: hsl(356,67%,50%)} + +/* With class=dim, elements that are no longer active are grayed out. */ +.full .incremental > .visited.dim, +.full .incremental.dim > .visited, +.full .dim .incremental > .visited, +.full.dim .incremental > .visited, +.full .overlay > .visited.dim, +.full .overlay.dim > .visited, +.full .dim .overlay > .visited, +.full.dim .overlay > .visited, +.full .next.visited.dim, +.full .dim .next.visited, +.full.dim .next.visited {opacity: 0.3} + +/* Animate the active element when it appears. By default, the element + is progressively revealed, starting from the left. Setting + class=emerge instead causes the element to go from transparent to + opaque. And class=quick omits the animation. The class can be set + on the element itself or on any ancestor, including on BODY. .*/ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .incremental > .active, .full .overlay > .active, + .full .next.active {animation: unfold 1s} + .full .incremental > .active.emerge, .full .overlay > .active.emerge, + .full .incremental.emerge > .active, .full .overlay.emerge > .active, + .full .emerge .incremental > .active, .full .emerge .overlay > .active, + .full.emerge .incremental > .active, .full.emerge .overlay > .active, + .full .emerge .next.active, .full .next.active.emerge, + .full.emerge .next.active {animation: fade-in 0.5s} + .full .incremental .active.quick, .full .overlay .active.quick, + .full .incremental.quick .active, .full .overlay.quick .active, + .full.quick .incremental .active, .full.quick .overlay .active, + .full .quick .incremental .active, .full .quick .overlay .active, + .full .quick .next.active, .full .next.active.quick, + .full.quick .next.active {animation: none} +} + +@keyframes unfold { + from {clip-path: inset(0% 100% 0% -100%)} + to {clip-path: inset(0% 0% 0% -100%)} +} + +/* Animation of a slowly growing element */ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + .full .grow {transition: 3s 1s ease-in-out transform; + position: relative; transform: scale(0.1); transform-origin: 0 50%} + .active .grow {transform: scale(1)} +} + +/* Transitions between slides */ +@media not screen and (prefers-reduced-motion: reduce) { + /* Experimental media query, see + https://www.w3.org/TR/2020/WD-mediaqueries-5-20200731/ */ + + .full .slide.active ~ .visited {animation: none} /* Moving backwards */ + + /* Transition: fade-in */ + .full .slide.fade-in.visited, + .fade-in .slide.visited {animation: delay 1s 1} + .full .slide.fade-in + .active, + .full .slide.fade-in + .comment + .active, + .fade-in .slide.active {animation: fade-in 1s 1} + @keyframes delay { + from {visibility: visible} + to {visibility: visible} + } + @keyframes fade-in { + from {opacity: 0} + to {opacity: 1} + } + + /* Transition: slide-in */ + .full .slide.slide-in.visited, + .slide-in .slide.visited {animation: leftout 1s 1} + .full .slide.slide-in + .active, + .full .slide.slide-in + .comment + .active, + .slide-in .slide.active {animation: leftin 1s 1} + @keyframes leftout { + from {transform: translate(0%, 0); visibility: visible; z-index: 1} + to {transform: translate(-100%, 0); visibility: visible; z-index: 1} + } + @keyframes leftin { + from {transform: translate(-100%, 0); visibility: visible} + to {transform: translate(0%, 0); visibility: visible} + } + + /* Transition: slide-out */ + .full .slide.slide-out.visited, + .slide-out .slide.visited {animation: leftout 1s 1} + .full .slide.slide-out + .active, + .full .slide.slide-out + .comment + .active, + .slide-out .slide.active {animation: do-nothing 1s 1} + @keyframes do-nothing { + from {z-index: 0} + to {z-index: 0} + } + + /* Transition: move-left */ + .full .slide.move-left.visited, + .move-left .slide.visited {animation: leftout 1s 1} + .full .slide.move-left + .active, + .full .slide.move-left + .comment + .active, + .move-left .slide.active {animation: rightin 1s 1} + @keyframes rightin { + from {transform: translate(100%, 0); visibility: visible} + to {transform: translate(0%, 0); visibility: visible} + } + + /* Transition: slide-up */ + .full .slide.slide-up.visited, + .slide-up .slide.visited {animation: topout ease-in 1s 1} + .full .slide.slide-up + .active, + .full .slide.slide-up + .comment + .active, + .slide-up .slide.active {animation: do-nothing ease-in 1s 1} + @keyframes topout { + from {transform: translate(0, 0%); visibility: visible; z-index: 1} + 80% {opacity: 1.0} + to {transform: translate(0, -100%); visibility: visible; opacity: 0.0; + z-index: 1} + } + /* Transition: move-up */ + .full .slide.move-up.visited, + .move-up .slide.visited {animation: topout ease-in 1s 1} + .full .slide.move-up + .active, + .full .slide.move-up + .comment + .active, + .move-up .slide.active {animation: bottomin ease-in 1s 1} + @keyframes bottomin { + from {transform: translate(0, 100%); visibility: visible} + to {transform: translate(0, 0%); visibility: visible} + } + + /* Transition: flip-up */ + .full {perspective: 1000px; perspective: 1000} + .full .slide.flip-up.visited, + .flip-up .slide.visited {animation: turn-down 1s 1 ease-in} + .full .slide.flip-up + .active, + .full .slide.flip-up + .comment + .active, + .flip-up .slide.active {animation: turn-up 1s 1 ease-out} + @keyframes turn-down { + from {transform: rotateX(0deg); visibility: visible} + 50%, to {transform: rotateX(90deg); visibility: hidden} + } + @keyframes turn-up { + from, 50% {transform: rotateX(-90deg); visibility: visible} + to {transform: rotateX(0deg); visibility: visible} + } + + /* Transition: flip-left */ + .full .slide.flip-left.visited, + .flip-left .slide.visited {animation: flip-left1 1s 1 ease-in} + .full .slide.flip-left + .active, + .full .slide.flip-left + .comment + .active, + .flip-left .slide.active {animation: flip-left2 1s 1 ease-out} + @keyframes flip-left1 { + from {transform: rotateY(0deg); visibility: visible} + 50%, to {transform: rotateY(-90deg); visibility: hidden} + } + @keyframes flip-left2 { + from, 50% {transform: rotateY(90deg); visibility: visible} + to {transform: rotateY(0deg); visibility: visible} + } + + /* Transition: center-out */ + .full .slide.center-out.visited, + .center-out .slide.visited {animation: gray 1s 1} + .full .slide.center-out + .active, + .full .slide.center-out + .comment + .active, + .center-out .slide.active {animation: center-out 1s 1} + @keyframes gray { + from, to {opacity: 0.5; visibility: visible} + } + @keyframes center-out { + from {clip-path: circle(0)} + to {clip-path: circle(100%)} + } + + /* Transition: wipe-left */ + .full .slide.wipe-left.visited, + .wipe-left .slide.visited {animation: gray 1s 1} + .full .slide.wipe-left + .active, + .full .slide.wipe-left + .comment + .active, + .wipe-left .slide.active {animation: rightin 1s 1} + + /* Transition: zigzag-left */ + .full .slide.zigzag-left.visited, + .zigzag-left .slide.visited {animation: gray 1s 1} + .full .slide.zigzag-left + .active, + .full .slide.zigzag-left + .comment + .active, + .zigzag-left .slide.active {animation: zigzag-left 1s 1} + @keyframes zigzag-left { + from {clip-path: + polygon(120% 0%, 120% 0%, 100% 30%, 120% 60%, 110% 100%, 120% 100%)} + to {clip-path: + polygon(120% 0%, 0% 0%, -20% 30%, 0% 60%, -10% 100%, 120% 100%)} + } + + /* Transition: zigzag-right */ + .full .slide.zigzag-right.visited, + .zigzag-right .slide.visited {animation: gray 1s 1} + .full .slide.zigzag-right + .active, + .full .slide.zigzag-right + .comment + .active, + .zigzag-right .slide.active {animation: zigzag-right 1s 1} + @keyframes zigzag-right { + from {clip-path: + polygon(-20% 0%, -20% 0%, 0% 30%, -20% 60%, -10% 100%, -20% 100%)} + to {clip-path: + polygon(-20% 0%, 100% 0%, 120% 30%, 100% 60%, 110% 100%, -20% 100%)} + } + + /* Transition: cut-in */ + .full .slide.cut-in.visited, + .cut-in .slide.visited {animation: gray 1s 1} + .full .slide.cut-in + .active, + .full .slide.cut-in + .comment + .active, + .cut-in .slide.active {animation: cut-in 1s 1} + @keyframes cut-in { + from {transform: translate(-100%, -100%)} + to {transform: translate(0%, 0%)} + } + +} /* End of @media not screen and (prefers-reduced-motion: reduce) */ + +/* A section with aria-live=assertive, which should be spoken, but not + displayed. (b6+ adds this style by itself, but Shower relies on the + style sheet setting it.)*/ +[role=region][aria-live=assertive] {position: absolute; top: 0; left: 0; + clip: rect(0 0 0 0)} + +/* Trick: If the viewport is exactly w x h or 1.2w x 1.2h, + it means the slides are + being shown inside an iframe of that size. Hide everything except + the targeted slide in that case and omit the black background, + which would otherwise be visible around the rounded corner of the + slide. (When JavaScript is on, adding ?full to the end of the slide + URL, e.g., ".../myslides.html?full#intro", has a similar effect and + doesn't require the iframe to be this exact size.) */ +@media (min-width: 40.839em /*= w - 0.05 */) and + (max-width: 40.939em /*= w + 0.05 */) and + (min-height: 22.95em /*= N - 0.05 */) and + (max-height: 23.05em /*= N + 0.05 */), + (min-width: 49.017em /*= 1.2 * w - 0.05 */) and + (max-width: 49.117em /*= 1.2 * w + 0.05 */) and + (min-height: 27.55em /*= 1.2 * N - 0.05 */) and + (max-height: 27.65em /*= 1.2 * N + 0.05 */) { + html {font-size: calc(100vh / 23)} + body {margin: 0; overflow: hidden} + body, .full {background: transparent} + body > *, .slide, .comment, .comment.long {display: none} + .slide {box-shadow: none; margin: 0} + .slide:target {display: block; outline: none} +} + +/* class=framed is used to indicate the slides are inside an iframe. */ +body.framed {background: transparent} +body.framed .slide {box-shadow: none} +body.framed .progress {display: none} + +/* class=has-2nd-window indicates this is a control window for another. (b6+) */ +.has-2nd-window {margin: 3px; background: #ddd; /*color: #000;*/ + font-size: calc((100vw - 17px - 2 * 3px) / 61.333 /*= 1.5 * w */)} +.has-2nd-window::after { /* To contain the floating slides */ + content: " "; display: block; height: 0; clear: left} +.has-2nd-window .slide:target {outline: none} +.has-2nd-window .comment::before {content: none} +.has-2nd-window .slide, +.has-2nd-window .comment {float: left; margin: 0.5em 0; + font-size: calc((100vw - 17px - 2 * 3px) / 97.778 /*= 2 * w + 16 */); + transition: 0.3s font-size} +.has-2nd-window .comment {box-shadow: none; background: none; color: #000} +.has-2nd-window .slide {clear: left} +.has-2nd-window .slide.active, .has-2nd-window .slide.active + .comment { + font-size: 1em} +.has-2nd-window .slide ~ .comment {width: 20.444em /*= w / 2 */} +.has-2nd-window .comment.long {float: none; clear: left; font-size: 1em; + width: 40.889em /*= w */} + +/* Outline elements on the second window that are incrementally + displayed on the first window (b6+) */ +.has-2nd-window .slide.active .incremental > *, +.has-2nd-window .slide.active .overlay > *, +.has-2nd-window .slide.active .next {outline: thin dashed red} + +/* Style for clocks on the second window or in index mode. */ +body {--time-factor: 0} /* Make sure it is defined, will be set by b6+ */ +.fullclock, .clock {position: fixed; z-index: 1; top: 0.5em; right: 0.5em; + background: linear-gradient(hsl(120,90%,20%), + hsl(120,80%,25%), hsl(120,90%,19%)); color: #fff; border-radius: 0.5em; + box-shadow: 0 2px 3px #000; text-align: center; width: fit-content} +.fullclock {padding: 0.3em; display: grid; justify-items: center; gap: 0.1em; + grid: "x y z" auto + "a b d" auto + "f c e" auto + "h c g" auto + / 1fr 1fr 1fr} +.fullclock time:nth-of-type(1) {grid-area: a; color: #9F9} +.fullclock time:nth-of-type(2) {grid-area: b} +.fullclock time:nth-of-type(3) {grid-area: d} +.fullclock .timepause {grid-area: g} +.fullclock .timeinc {grid-area: e} +.fullclock .timedec {grid-area: f} +.fullclock .timereset {grid-area: h} +.fullclock i:nth-of-type(1) {grid-area: x} +.fullclock i:nth-of-type(2) {grid-area: y} +.fullclock i:nth-of-type(3) {grid-area: z} +.fullclock > span {grid-area: c} +.fullclock time {padding: 0 0.3em} +.fullclock time b {font-family: OCR A Std, Orator Std, monospace; + font-size: 1.2em} +.fullclock i {font-size: 70%; font-style: normal; color: #9F9} +.fullclock button {width: 100%; font: 80%/1 Noto Sans Symbols, sans-serif} +/* The span is made into a pie chart that shows the fraction of time used. */ +.fullclock > span, .clock > span {display: inline-block; + width: 3.5em; height: 3.5em; border-radius: 50%; background: #FFF; + background: conic-gradient( + #000 calc(var(--time-factor) * 360deg), + #FFF calc(var(--time-factor) * 360deg), + #FFF 360deg), #FFF} +@supports not (background: conic-gradient( + #000 calc(var(--time-factor) * 360deg), + #FFF calc(var(--time-factor) * 360deg), + #FFF 360deg), #FFF) { + /* If pie chart not possible, show a clock hand that turns */ + .fullclock > span, .clock > span {position: relative; background: #FFF} + .fullclock > span > span, .clock > span > span {height: 2px; + width: 50%; background: #000; position: absolute; top: calc(50% - 1px); + left: 50%; transform-origin: 0 1px; + transform: rotate(calc(var(--time-factor) * 360deg - 90deg))} +} +.clock {padding: 0.3em; display: grid; justify-items: center; gap: 0.1em; + grid: "a a a a" auto + "c c e f" auto + "c c g h" auto + / 1fr 1fr 1fr 1fr} +.clock time {grid-area: a; padding: 0 0.3em} +.clock .timepause {grid-area: g} +.clock .timedec {grid-area: e} +.clock .timeinc {grid-area: f} +.clock .timereset {grid-area: h} +.clock > span {grid-area: c} +.clock time b {font-family: OCR A Std, Orator Std, monospace; + font-size: 1.2em} +.clock button {width: 100%; font: 80%/1 Noto Sans Symbols, sans-serif} + +/* When time is nearly up, make the clock orange. */ +body.time-warning .fullclock, body.time-warning .clock {background: + linear-gradient(hsl(33,100%,37%), hsl(33,90%,42%), hsl(33,100%,36%))} +/* When time is up, make the clock red. */ +body[data-time-factor="100"] .fullclock, +body[data-time-factor="100"] .clock {background: + linear-gradient(hsl(0,100%,47%), hsl(0,90%,55%), hsl(0,100%,46%))} + +/* Make the clock blue when it is paused. */ +body.paused .fullclock, body.paused .clock { + background: linear-gradient(hsl(240,85%,55%), hsl(240,80%,60%), + hsl(240,85%,54%))} +body:not(.paused) .timepause :nth-child(1) {display: none} +body.paused .timepause :nth-child(2) {display: none} +body.paused .timepause {opacity: 0.6} + +/* A div with class=ui generated by b6+, containing play, help and + other buttons. */ +.b6-ui {position: fixed; bottom: 0; left: 0; right: 0; z-index: 1; + background: hsl(205,100%,20%); color: white; display: flex; flex-wrap: wrap; + padding: 0.2em; gap: 0.5em 0; justify-content: center; + box-shadow: 0 0 4px #111} +.b6-ui button {flex: 7.5em 0.03; line-height: 1.2; background: none; + color: inherit; border: none; border-radius: 0.3em} +@media (min-width: 37.35em) and (min-height: 32em) { + .b6-ui span {display: block} + .b6-ui span:first-child {font-size: 200%} +} +.b6-ui button:hover, .b6-ui button:focus {background: hsl(205,100%,17%)} + +/* White-on-black pages, when the user has selected dark mode in the OS */ +@media (prefers-color-scheme: dark) { + .slide {color: white; background-image: url(side-banner-dark.webp); + background-color: black; + text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black} + .slide.cover {background-color: black; + background-image: url(banner-dark.webp), linear-gradient(#afb4bc, #afb4bc), + url(banner-dark.webp), linear-gradient(#afb4bc, #afb4bc)} + .slide.cover p {color: #444} + .slide.final {background-color: black; + background-image: url(banner-dark.webp), linear-gradient(#afb4bc, #afb4bc)} + .slide.final::after {color: black; + background: 50% 1.71em / auto 1.55em no-repeat + url(data:image/svg+xml,)} + .slide address {color: hsl(356,90%,60%)} + address :link, address :visited {color: inherit} + .slide ul > li::before, .slide code {background: #333} + .slide :link {color: inherit; + background: hsla(240,100%,20%,0.3)} + .slide :visited {color: inherit; + background: hsla(270,100%,20%,0.3)} + .slide :link, .slide :visited {padding: 0.1em 0.3em; + border-radius: 0.3em} + .slide em {background: hsl(322,100%,40%)} + .slide h3 {color: hsl(62,100%,60%)} + table.striped tr:nth-child(2n+2) {background: #333} + .comment.long {background: #333; color: white} + .has-2nd-window {background: #333} + .has-2nd-window .comment {color: white} + .b6-ui {} + .b6-ui button {} +} + +/* To tell the b6plus.js script that this style sheet supports class + darkmode on BODY, set a special property on an element with class + "has-darkmode". */ +.has-darkmode {--has-darkmode: 1} + +/* White-on-black pages, with class=darkmode on body or on a slide. + Same colors as for 'prefers-color-scheme: dark' above, except that + the colors of form elements do not change. (They are set by the UA + style sheet.) */ +.darkmode.slide, .darkmode .slide {color: white; background-color: black; + background-image: url(side-banner-dark.webp); + text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black} +.darkmode.cover, .darkmode .cover {background-image: url(banner-dark.webp), + linear-gradient(#afb4bc, #afb4bc), url(banner-dark.webp), + linear-gradient(#afb4bc, #afb4bc)} +.darkmode.cover p, .darkmode .cover p {color: #444} +.darkmode.final, .darkmode .final {background-image: url(banner-dark.webp), + linear-gradient(#afb4bc, #afb4bc)} +.darkmode.final::after, .darkmode .final::after {color: black; + background: 50% 1.71em / auto 1.55em no-repeat + url(data:image/svg+xml,)} +.darkmode.slide address, .darkmode .slide address {color: hsl(356,90%,60%)} +.darkmode address :link, .darkmode address :visited {color: inherit} +.darkmode.slide ul > li::before, .darkmode.slide code, +.darkmode .slide ul > li::before, .darkmode .slide code {background: #333} +.darkmode.slide :link, .darkmode .slide :link {color: inherit; + background: hsla(240,100%,20%,0.3)} +.darkmode.slide :visited, .darkmode .slide :visited {color: inherit; + background: hsla(270,100%,20%,0.3)} +.darkmode.slide :link, .darkmode.slide :visited, +.darkmode .slide :link, .darkmode .slide :visited {padding: 0.1em 0.3em; + border-radius: 0.3em} +.darkmode.slide em, .darkmode .slide em {background: hsl(322,100%,40%)} +.darkmode.slide h3, .darkmode .slide h3 {color: hsl(62,100%,60%)} +.darkmode.slide table.striped tr:nth-child(2n+2), +.darkmode .slide table.striped tr:nth-child(2n+2) {background: #333} +.darkmode .comment.long {background: #333; color: white} +.darkmode.has-2nd-window {background: #333} +.darkmode.has-2nd-window .comment {color: white} +.darkmode .b6-ui {} +.darkmode .b6-ui button {} + +/* Black-on-white in case class=lightmode was set on the slide or on body */ +.lightmode.slide, .lightmode .slide:not(.darkmode) {color: black; + background-color: hsl(0,0%,98%); background-image: url(side-banner.webp); + text-shadow: 0 0 1px hsl(0,0%,98%), 0 0 1px hsl(0,0%,98%), + 0 0 1px hsl(0,0%,98%), 0 0 1px hsl(0,0%,98%)} +.lightmode.cover, .lightmode .cover:not(.darkmode) { + background-image: url(banner.webp), linear-gradient(#e7f1fa, #e7f1fa), + url(banner.webp), linear-gradient(#e7f1fa, #e7f1fa)} +.lightmode.cover p, .lightmode .cover:not(.darkmode) p {color: #6b6b6b} +.lightmode.final, .lightmode .final:not(.darkmode) { + background-image: url(banner.webp), linear-gradient(#e7f1fa, #e7f1fa)} +.lightmode.final::after, .lightmode .final:not(.darkmode)::after {color: white; + background: 50% 1.71em / auto 1.55em no-repeat + url(data:image/svg+xml,)} +.lightmode.slide address, .lightmode .slide:not(.darkmode) address { + color: hsl(356,67%,40%)} +.lightmode.slide address :link, .lightmode.slide address :visited, +.lightmode .slide:not(.darkmode) address :link, +.lightmode .slide:not(.darkmode) address :visited {color: inherit} +.lightmode.slide ul > li::before, .lightmode.slide code, +.lightmode .slide:not(.darkmode) ul > li::before, +.lightmode .slide:not(.darkmode) code {background: #eee} +.lightmode.slide :link, .lightmode .slide:not(.darkmode) :link { + color: #00e; color: linktext; background: none} +.lightmode.slide :visited, .lightmode .slide:not(.darkmode) :visited { + color: #609; color: visitedtext; background: none} +.lightmode.slide :link, .lightmode.slide :visited, +.lightmode .slide:not(.darkmode) :link, +.lightmode .slide:not(.darkmode) :visited {padding: initial; + border-radius: initial} +.lightmode.slide em, .lightmode .slide:not(.darkmode) em { + background: hsl(62,100%,50%)} +.lightmode.slide table.striped tr:nth-child(2n+2), +.lightmode .slide:not(.darkmode) table.striped tr:nth-child(2n+2) { + background: #EEE} +.lightmode .comment.long {background: white; color: black} +.lightmode.has-2nd-window {background: #ddd} +.lightmode.has-2nd-window .comment {color: #000} +.lightmode .b6-ui {} +.lightmode .b6-ui button {} + +/* Printing. */ +@page { + margin: 1cm; + @bottom {content: counter(page)} +} +@media print { + html {font-size: 10pt} + body {background: none; color: black; margin: 0; columns: 40.889em /*= w */; + column-gap: 4em; column-rule: 0.2pt solid} + .slide {border: 0.2pt solid black; margin: 2em auto; display: block; + border-radius: 0; + overflow: hidden; break-inside: avoid; box-shadow: none} + .comment {background: none; color: black; padding: 0; + columns: 25em; column-rule: thin solid; column-gap: 2em; + widows: 2; orphans: 2; width: auto; height: auto; display: block; + border-radius: 0; overflow: auto; + margin: 2em 1em 2em 0; box-shadow: none} + .comment:before {content: none} + .slide summary::before {content: none} + .slide details {visibility: hidden} + .slide summary {visibility: visible} + [role=region][aria-live=assertive] {display: none} +} + +/* Output to PDF (trick). + + To output to PDF, print the slides to PDF while selecting a + landscape paper size, e.g. A4 landscape or Letter landscape. + + This style sheet assumes that, when the output is in landscape + mode, the goal is to export one slide per page, without margins, + and omitting the comments between the slides. (On the other hand, + to output multiple slides per page and interleave the comments, + choose a page size in portrait mode.) + + Note: Not all user agents respect the 'size' property to set the + size of the output. If they don't, there will be some margin + to the right and below each slide. Prince respects the property. + E.g, to make myslides.pdf from myslides.html: + + prince --page-size=landscape myslides.html + + W3C team can also use the ",pdfui" tool online. +*/ +@media print and (orientation: landscape) { + html {font-size: 7mm} + .comment, .comment.long {display: none} + .slide {margin: 0; page-break-after: always; box-shadow: none; border: none} + + @page { + size: 286.22mm /*= 7 * w */ 161mm /*= 7 * N */; + margin: 0; + @bottom {content: none} + } +} +@media print and (orientation: landscape) and (min-width: 11in) { + /* Letter-size paper */ + html {font-size: 0.26902in /*= 11 / w */} + @page {size: 11in 6.1875in /*= 11 / A */} +} +@media print and (orientation: landscape) and (min-width: 296mm) { + /* A4-size paper */ + html {font-size: 7.2636mm /*= 297 / w */} + @page {size: 297mm 167.06mm /*= 297 / A */} +} diff --git a/presentations/ac2024/Templates/w3c-white-blue-circle.svg b/presentations/ac2024/Templates/w3c-white-blue-circle.svg new file mode 100644 index 0000000..17f4970 --- /dev/null +++ b/presentations/ac2024/Templates/w3c-white-blue-circle.svg @@ -0,0 +1,22 @@ + + + + + + image/svg+xml + + W3C + + + + + + W3C + + + + + + + + diff --git a/presentations/ac2024/awkufcp.jpeg b/presentations/ac2024/awkufcp.jpeg new file mode 100644 index 0000000..0245270 Binary files /dev/null and b/presentations/ac2024/awkufcp.jpeg differ diff --git a/presentations/ac2024/ext-menu-a.png b/presentations/ac2024/ext-menu-a.png new file mode 100644 index 0000000..526bc9f Binary files /dev/null and b/presentations/ac2024/ext-menu-a.png differ diff --git a/presentations/ac2024/ext-menu-b.png b/presentations/ac2024/ext-menu-b.png new file mode 100644 index 0000000..a9eb504 Binary files /dev/null and b/presentations/ac2024/ext-menu-b.png differ diff --git a/presentations/ac2024/ext01.png b/presentations/ac2024/ext01.png new file mode 100644 index 0000000..58077bc Binary files /dev/null and b/presentations/ac2024/ext01.png differ diff --git a/presentations/ac2024/ext02.png b/presentations/ac2024/ext02.png new file mode 100644 index 0000000..854bc4a Binary files /dev/null and b/presentations/ac2024/ext02.png differ diff --git a/presentations/ac2024/ext02a.png b/presentations/ac2024/ext02a.png new file mode 100644 index 0000000..7e471bf Binary files /dev/null and b/presentations/ac2024/ext02a.png differ diff --git a/presentations/ac2024/ext03.png b/presentations/ac2024/ext03.png new file mode 100644 index 0000000..b995f7b Binary files /dev/null and b/presentations/ac2024/ext03.png differ diff --git a/presentations/ac2024/index.html b/presentations/ac2024/index.html new file mode 100644 index 0000000..ae114e7 --- /dev/null +++ b/presentations/ac2024/index.html @@ -0,0 +1,394 @@ + + + + + + + + Exploring making site navigation more accessible, with "well-known destinations" + + + + + + + + + + + + +

+ + +
+ + + + + +
+ Leaving slide mode. +
+ + +
+

Exploring making site navigation more accessible, with "well-known destinations"

+
+ WAI-Adapt Task Force
+ Matthew Atkinson
+ Samsung R&D Institute UK +
+

W3C + AC 2024
Hiroshima, Japan
hybrid meeting
8–9 APRIL 2024

+
+ + +
+

🚧

+
+ +
+

Please note, this proposal is under construction. Feedback is very welcome.

+
+ + +
+

What?

+
+ + +
+

The Adapt TF is proposing a standard, + mechanical, way + for sites to state which popular pages they offer.

+

This allows user agents to + automatically discover which of these popular pages a + site provides.

+
+ + +
+

"Well-known…

+
+ + +
+

…Destinations"

+
+ + +
+
+

🕸️ Port 80

+
+
+ +
+

Well-known locations are a fundamental technique in computing. For example, web servers run on port 80 by + default, to make them easily discoverable.

+
+ + +
+
+

🔎 "search"

+
+
+ +
+

A magnifying glass icon often indicates where you will find the search feature of a site or app.

+
+ + +
+

🏡

+
+ +
+

When we talk about destinations, in terms of popular pages on sites, what do we mean? Well there's the home + page….

+
+ + +
+

🛒

+
+ +
+

…the products page…

+
+ + +
+

📧

+
+ +
+

…the contact page, and so on.

+
+ + +
+

Why?

+
+ +
+

But why do we need to make these pages easier to find? There are several reasons; here is one big one…

+
+ + +
+

Variation in terms

+ +
+ +
+

There are many different ways that the action of logging into one's account could be phrased—including in + other languages, with which the user may be unfamiliar.

+
+ + +
+
+ +
+

The design of a site may be visually confusing—here is an example of a footer with low contrast.

+
+ + +
+

How?

+
+ + +
+

https://example.site/.well-known/...

+
+ +
+

There's a standard for well-known URLs.

+
+ + +
+ A Well-known URL for Changing Passwords - W3C spec (top of document) +

IETF:
RFC 8615 +

IANA:
Registry +

+ +
+

We (the Accessible Platform Architectures Working Group) first learnt of this when reviewing the W3C spec &qout;A + Well-known URL for Changing Passwords". The spec is based on an existing standard (RFC) from IETF, and IANA + maintains the registry of well-known URLs.

+
+ + +
+ +
+ +
+

Here are some (but not all) of the well-known URLs we may specify.

+
+ + +
+ +
+ +
+

Their full URL paths from the root would be like this. We have namespaced them under the path "ia".

+
+ + +
+

+ ia + information architecture +

+
+ +
+

The "ia" path component stands for "information architecture"—this is accessibility and + then some.

+

This also allows us to facilitate more efficient discovery of the pages a site provides.

+
+ + +
+
+
+

We don't show users these URLs.

+

This is just about communication between the + user agent, and the web server. +

+
+
+
+ +
+

Users won't routinely see these URLs—the purpose of standardising them is so that the user agent and site + can communicate about which well-known destinations are offered.

+
+ + +
+

What might it look like for users?

+
+ +
+

We developed a demo browser extension to show how a UI based on this might look.

+
+ + +
+ Fictional ACME Inc. home page. Extension icon in browser toolbar has a badge that says '6' on it. +
+ +
+

Here's the home page of a site. The extension's toolbar icon badge shows there are 6 well-known destinations + offered by this site.

+
+ + +
+ The ACME Inc. home page, with the extension pop-up open, showing 6 buttons, each containing emoji and accompanying text names for the well-known destinations offered by the site: home, accessibility statement, contact, help, log in, and products. +
+ +
+

This site offers 6 well-known destinations, as shown—in the user's preferred terms— in the + extension's pop-up menu.

+
+ + +
+ As with the previous image, but an arrow is shown pointing to the 'log in' destination, indicating its button is to be activated. +
+ +
+

If we activate one of the buttons…

+
+ + +
+ The ACME Inc. log in page (called 'Sign in' on the page itself). +
+ +
+

…we are navigated to the corresponding page—though the site calls this 'Sign in', the extension + called it 'Log in', according to the user's preferred term.

+
+ + +
+
+ A close-up of the extension pop-up for the ACME Inc. home page, with the 6 buttons, each containing emoji and accompanying text names for the well-known destinations offered by the site: home, accessibility statement, contact, help, log in, and products. +
+
+ +
+

Different sites can support different destinations; this is a zoomed view of the menu for the site we were just + looking at.

+
+ + +
+
+ A close-up of the pop-up for another site, which supports 5 destinations, one of which is new: home, accessibility statement, contact, help, and search. +
+
+ +
+

This site is about providing information, so it doesn't have products, or log in, but it does have a search + destination.

+
+ + +
+

Next steps

+ +
+ +
+

As mentioned, we're at the beginning of this work. Coming up are these three stages. As part of exploring the + minimum viable product, we are thinking about how standard HTML features could be extended to support highlighting + the relevant area of the page, once the user gets there.

+
+ + +
+

Join us!

+ +
+ +
+

Please join us and participate in empowering more people to use the web independently. Please ask any questions. + Feedback is greatly appreciated. Thank you for reading!

+
+ + diff --git a/presentations/ac2024/wkd.css b/presentations/ac2024/wkd.css new file mode 100644 index 0000000..80bf920 --- /dev/null +++ b/presentations/ac2024/wkd.css @@ -0,0 +1,72 @@ +.slide a.flat { + padding: 0; +} + +.normaltext { + font-weight: normal; +} + +.initial-letter-emphasis { + display: inline-block; +} + +.initial-letter-emphasis::first-letter { + text-decoration: underline; +} + +strong, +.initial-letter-emphasis::first-letter { + color: blue; +} + +@media (prefers-color-scheme: dark) { + + strong, + .initial-letter-emphasis::first-letter { + color: yellow; + } +} + +.centre { + display: grid; + height: 100%; + place-items: center; +} + +.horiz-centre { + text-align: center; +} + +.bigger { + font-size: 1.42em; +} + +.large { + font-size: 2em; +} + +.footer-split { + display: flex; + flex-direction: column; + height: 100%; +} + +.footer-split li, +.footer-split p { + text-shadow: none; +} + +.footer-split>* { + flex-basis: 100%; + padding: 1em; +} + +.footer-split>div:first-child { + background: white; + color: black; +} + +.footer-split>div:last-child { + background: grey; + color: lightgray; +} \ No newline at end of file