HTML Slides with notes

... 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!

HTML Slides with notes

... 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!

But first, some attribution

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)

Let's define our slide

  <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)

CSS to make a slide fit the screen

  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.

Grab all slides

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!

Start at first slide

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.

Switch slides on keypress

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.

Note support (1)

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.

Note support (2)

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

Note support (3)

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

Note support (4)

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.

Golfing time ⛳

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

Golfing time ⛳

 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.

Golfing time - continued ⛳

 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.