... in 22 lines of JavaScript
let slides = [...document.getElementsByClassName("slide")] .map((slide, i) => [ slide, (i = slide.nextElementSibling)?.className === "slidenote" ? i : slide ]), current = 0 viewSlides = 0, jump = () => slides[current][viewSlides].scrollIntoView(), bc = new BroadcastChannel("slide_switching"), l = slides.length-1; bc.onmessage = ({data}) => { viewSlides = 1 ^ data.viewSlides; current = data.current; jump(); }; document.addEventListener("keypress", ({key}) => { current += (key == "j") - (key == "k"); current = current < 0 ? 0 : current > l : l : current; viewSlides ^= (key == "n"); bc.postMessage({current, viewSlides}); jump(); });
(Use j and k to navigate, n to swap notes and slides)
Don't worry, I'll walk you all through it in a readable font size!
... in 22 lines, or 371 bytes of JavaScript
let a=[...document.getElementsByClassName("slide")].map((a,b)=>[ a,"slidenote"==(b=a.nextElementSibling)?.className?b:a]),b=0,c=0, d=()=>a[b][c].scrollIntoView(),e=new BroadcastChannel("s"),l=a. length-1;d();e.onmessage=({data:a})=>{c^=a.c,b=a.b,d()};document. addEventListener("keypress",({key:f})=>{b+=(f=="j")-(f== "k");b=b<0?0:b>l?l:b;c^=f=="n";e.postMessage({c,b});d()})
... but not before pointing out that this really minifies to super-tiny code - and still is practical to use!
This code builds on minslides (https://ratfactor.com/minslides/) by Dave Gaur.
// golfed minslides, 173 bytes let a=document.getElementsByClassName("slide"),b=0,c=a.length-1; document.addEventListener("keypress",({key:d})=>{b+=("j"==d)- ("k"==d),b=0>b?0:b>l?l:b,a[b].scrollIntoView()})
My addition: notes in a second window
(Also the hand-optimized minifaction)
I want to credit Dave Gaur for coming up with minslides.
For me it was yet another reminder that browsers are "batteries included" for surprisingly many scenarios as long as you bother to dig into the APIs available.
Case-in-point: it turned out to be ridiculously simple to add note support in a second window!
Here, let me show you by switching between notes and slides with a keypress (n
, to be exact)
<div class="slide"> Anything in here is one slide (who needs components?) </div> <div class="slidenote"> (optional) Anything in here is a slide note for the slide above </div>
Notes are optional, but must follow the slides that they are for. To switch to the note view, press n
So how to determine is something is a slide? Well, why not use plain old <div>
with a slide
class? And let's define slide notes as a div
with a slidenote
class. Easy.
This also lets us put anything in our slide that we are allowed to put into a plain webpage.
Also, we can write a plain old markdown file (or whatever you prefer), and separate the slides by inserting those div tags (like I did for these slides, actually)
div.slide, div.slidenote { height: 100vh; width: 100vw; /* Other slide styling options below */ ... ... }
Yes, it's that simple
Of course, we want our slides to be exactly as big as the screen in full-screen, and as big as the window when windowed.
Turns out modern CSS has a ludicrously simple way to do that.
let slides = document.getElementsByClassName("slide");
getElementsByClassName
returns an array-like object with all children of the node it is called on (or the entire page is called on document, like we're doing here).
Conveniently, these children will be in the order that they appear in the document
So we now have an array of all slides, in order. Nice!
let slides = document.getElementsByClassName("slide"), current = 0, jump = () => slides[current].scrollIntoView(); jump();
By default, scrollIntoView()
instantly jumps to the element that it is called on. And since our slide divs fill the screen, that is equivalent to changing a slide. Convenient!
We can sneakily put other stuff around our slides and nobody will see it as long as we avoid scrolling manually!
How many of you knew of scrollIntoView
? For the record, it's been supported since IE 8, Chrome 1 and Firefox 1.
document.addEventListener("keypress", ({key}) => { if(key === "j") current++; if(key === "k") current--; if(current < 0) current = 0; if(current >= slides.length) current = slides.length - 1; jump(); });
And with that we already have basic slide functionality!
All we have to do now for feature parity with minslides is add navigation.
Just add a listener for keypress events, change our current
variable and jump.
Yes, it's that simple.
First version: add a notes
array synced to the slides
array-like
const notes = [...slides].map(slide => { const note = slide.nextElementSibling; return note?.className === "slidenote" ? note : slide; });
Ok, so to support notes, we want to check if a slide is followed by a div with slidenote
class.
Luckily, each element has a nextElementSibling
property that lets us fetch the node that follows it (if any).
Then we simply check that sibling's class name (if it exists, hence the optional chaining)
Also, remember how slides
is an array-like instead of an actual array? Because of that we can't directly use map on it,
so we force it into an actual array with the spread syntax.
Modify the jump function to pick slides or notes depending on view mode:
let slides = document.getElementsByClassName("slide"), current = 0, viewSlides = true, jump = () => { if (viewSlides) slides[current].scrollIntoView(); else notes[current].scrollIntoView(); }; jump();
Now we modify the jump function to jump to slides or notes depending on our view mode.
Straightforward
Add key to switch modes
document.addEventListener("keypress", ({key}) => { if(key === "j") current++; if(key === "k") current--; if(current < 0){ current = 0; } if(current >= slides.length){ current = slides.length - 1; } if(key === "n") viewSlides = !viewSlides; jump(); });
Of course, then we also need a way to switch these mode. Let's pick n
for notes
const bc = new BroadcastChannel("slide_switching_channel"); bc.onmessage = ({data}) => { current = data.current; viewSlides = !data.viewSlides; jump(); }; document.addEventListener("keypress", ({key}) => { /* ... the previous stuff ... */ bc.postMessage({current, viewSlides}); });
Done! Multiple windows supported!
Now all we're missing is having notes in a separate window synchronized with the slides.
For that we can use a BroadcastChannel
to communicate state between open windows on the same url. It's basically event-based message passing.
When we receive an event, we sync the current
value, then look at the view mode of the sender and pick the opposite, then jump.
So if the "notes" window has focus and moves a slide, the "slides" window is synced and forced to slide view, and vice versa.
Replace notes
with [slide, note]
pairs, also shortens jump()
code
let slides = [...document.getElementsByClassName("slide")] .map((slide, i) => [ slide, (i = slide.nextElementSibling)?.className == "slidenote" ? i : slide ]), current = 0, viewSlides = 0, jump = () => slides[current][viewSlides].scrollIntoView()
Ok, that's the readable version, but not the version I showed on the first slides. If I have time left I can explain the golf abuse, otherwise you'll have to read these slides on the internet later.
First, let's get rid of that branch in jump
by zipping slides and notes into one array of pairs, picking between the pair based on view mode.
Also, yes, we're abusing the fact that map
lets us pass the index as a second parameter to shave some characters off of initializing a temporary variable
bc.onmessage = ({data}) => { viewSlides = data.viewSlides^1; current = data.current; jump(); }; document.addEventListener("keypress", ({key}) => { if (key == "j") ++current; if (key == "k") --current; if (current < 0) current = 0; if (current >= slides.length) current = slides.length - 1; if (key == "n") viewSlides ^= 1; bc.postMessage({current, viewSlides}); jump(); });
We made viewSlides
an integer, so we'll use xor bitmasking to switch it between zero and one.
let ..., l = slides.length-1; ... document.addEventListener("keypress", ({key}) => { current += (key == "j") - (key == "k"); current = current < 0 ? 0 : current > l : l : current; viewSlides ^= (key == "n"); bc.postMessage({current, viewSlides}); jump(); });
Throw it through a minifier, do a bit of clean-up, and it's 371 bytes of JavaScript
And that's basically it! Throw this whole thing through a minifier (wrapped in a function so it's free to mangle variable names), do a bit of clean-up, and we end up with the 371 character minified version I showed at the beginning.