Zum Inhalt springen
aviral gupta

// B6.5 · ca. 38 Min. · Einstieg

Projekt: eine Bildergalerie

Sie bauen eine Galerie aus einem Array von Fotos: render() zeichnet sie, ein Listener auf den Vorschaubildern wählt ein Foto, und Buttons und Pfeiltasten blättern durch die Fotos und laufen um.

Lektion 5 von 5 in B6 Das DOM und Ereignisse

Ende des Moduls

Danach können Sie

  • Die Galerie mit render() aus einem Daten-Array bauen: Vorschaubilder, Hauptbild und Bildunterschrift
  • Ein Foto mit einem delegierten Listener wählen und das aktuelle Vorschaubild mit einer Klasse und aria-current markieren
  • Mit Zurück, Weiter und den Pfeiltasten durch die Fotos blättern und mit % am Ende umlaufen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B6.4. Der Klick landet auf dem Icon im Button. Was gibt dieser Code aus?

    const bar = document.querySelector('#bar');
    bar.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      console.log(event.target.id, button.id, event.currentTarget.id);
    });
    document.querySelector('#icon').click();
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Die Galerie zeigt das letzte von drei Fotos, und der Nutzer drückt Weiter. Was gibt dieser Code aus?

    const photos = ['fox', 'owl', 'heron'];
    let current = 2;
    current = (current + 1) % photos.length;
    console.log(photos[current]);
  3. Üben · Aufgabe 3 von 7

    render() läuft womöglich oft. Ergänzen Sie die Methode, die die neuen Einträge einsetzt und die alten entfernt, sodass die Liste nie doppelt wächst.

    thumbs.____(...items);
    thumbs.(...items);
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Teil der Galerie seine Aufgabe zu.

  5. Üben · Aufgabe 5 von 7

    Was gibt dieser Code aus?

    const thumbs = document.querySelector('#thumbs');
    const photos = ['fox', 'owl', 'heron'];
    let current = 1;
    const items = photos.map((name, index) => {
      const button = document.createElement('button');
      button.textContent = name;
      if (index === current) button.setAttribute('aria-current', 'true');
      return button;
    });
    thumbs.replaceChildren(...items);
    console.log(items.map((b) => String(b.getAttribute('aria-current'))).join(' '));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Vorschaubild ist <button data-index="1"><img id="owl" …></button>, und der Klick landet auf dem Bild. Was gibt dieser Code aus?

    const photos = ['fox', 'owl', 'heron'];
    const thumbs = document.querySelector('#thumbs');
    thumbs.addEventListener('click', (event) => {
      const index = Number(event.target.dataset.index);
      console.log(index, photos[index]);
    });
    document.querySelector('#owl').click();
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: Erweitern Sie Ihre Galerie. Fügen Sie einen Absatz #position hinzu, der für das aktuelle Foto "Photo 2 of 3" zeigt; aktualisieren Sie ihn in render(), damit er bei jeder Art des Blätterns stimmt. Lassen Sie die Taste Home das erste Foto zeigen und End das letzte, im selben keydown-Listener wie die Pfeiltasten, über show(). Prüfen Sie von Hand, dass ArrowRight nach End zum ersten Foto umläuft. Probieren Sie es über einen lokalen Webserver aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Die Galerie von Anfang bis Ende

Die Seite hat ein leeres Hauptbild und eine leere Bildunterschrift, Buttons für Zurück und Weiter und eine leere Liste. main.js hält drei Fotos als Daten und baut alles mit render(). Ein Listener auf der Liste wählt ein Foto, show() lässt den Index umlaufen, und die Buttons und Pfeiltasten rufen show() auf. Am Ende klickt das Skript das Bild des dritten Vorschaubilds, dann Weiter, und drückt ArrowLeft, so wie es die Tests der Übungen tun. Die Bilder sind kleine SVG-Zeichnungen in data:-URLs, erzeugt von picture(), es gibt also keine Bilddateien zu laden.

main.js

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

// One listener for all thumbnails, also the ones render() builds later.
thumbs.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !thumbs.contains(button)) return;
  current = Number(button.dataset.index);
  render();
});

function show(index) {
  current = (index + photos.length) % photos.length; // wrap around at both ends
  render();
}

document.querySelector('#prev').addEventListener('click', () => show(current - 1));
document.querySelector('#next').addEventListener('click', () => show(current + 1));
document.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowLeft') show(current - 1);
  if (event.key === 'ArrowRight') show(current + 1);
});

render();

// The script tries its own page, the way the exercise tests do.
thumbs.querySelectorAll('img')[2].click();
console.log('thumbnail 3:', caption.textContent);
document.querySelector('#next').click();
console.log('Next:', caption.textContent);
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
console.log('ArrowLeft:', mainImage.getAttribute('alt'));
console.log('current thumbnail:', thumbs.querySelector('[aria-current="true"]').dataset.index);

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery</title>
</head>
<body>
  <h1>Gallery</h1>
  <figure>
    <img id="main" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22/%3E" alt="">
    <figcaption id="caption"></figcaption>
  </figure>
  <button id="prev" type="button">Previous</button>
  <button id="next" type="button">Next</button>
  <ul id="thumbs"></ul>
  <script type="module" src="main.js"></script>
</body>
</html>

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Ausgabe

thumbnail 3: A heron by the river
Next: A red fox in the snow
ArrowLeft: A heron by the river
current thumbnail: 2
<!-- #caption -->
<figcaption id="caption">A heron by the river</figcaption>
  • Der Klick landete auf dem img; closest('button') fand seinen Button, und data-index "2" wurde zur Zahl 2.
  • Der Schnappschuss zeigt die Bildunterschrift, die render() baut; die src-Werte der Vorschaubilder sind lange data:-URLs, deshalb gibt das Skript stattdessen den Index des aktuellen Vorschaubilds aus.
  • Weiter nach dem Reiher lief zum Fuchs um: (2 + 1) % 3 ist 0.
  • ArrowLeft, an body geschickt, stieg zum Listener auf document auf, und (0 - 1 + 3) % 3 ist 2: wieder der Reiher.
  • Genau ein Button, der dritte, hat aria-current="true"; render() hat alle drei neu gebaut.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Übungen

Übung 1 von 3

Die Galerie aus den Daten rendern

Vervollständigen Sie render(). Jedes Foto wird ein li mit einem Button mit type="button" und data-index gleich seinem Index, und im Button ein img mit src und alt des Fotos. Der Button des aktuellen Fotos, und nur dieser, bekommt die Klasse selected und aria-current="true". Das Hauptbild #main bekommt src und alt des aktuellen Fotos, und #caption zeigt seinen alt-Text als Text. Der Starter setzt nackte Bilder ohne alt in die Liste und lässt die Hauptansicht unverändert.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die Liste enthält Bilder ohne Buttons und ohne alt, und Hauptbild und Bildunterschrift bleiben unverändert.

  2. Hinweis 2

    Nehmen Sie im map-Callback (photo, index). Erzeugen Sie img, Button und li, setzen Sie button.dataset.index = String(index), und hängen Sie das img an den Button und den Button an das li.

  3. Hinweis 3

    if (index === current) fügen Sie die Klasse hinzu und setAttribute('aria-current', 'true'). Setzen Sie nach replaceChildren src und alt von #main mit setAttribute und das textContent der Bildunterschrift.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    const li = document.createElement('li');
    li.append(img);
    return li;
  });
  thumbs.replaceChildren(...items);
  // TODO: put each image in a button with data-index, give the image its alt,
  // mark the current button, and show the current photo with its caption.
}

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const buttons = (page) => [...page.document.querySelectorAll('#thumbs li > button')];
const images = (page) => [...page.document.querySelectorAll('#thumbs li > button > img')];

test('Jedes Foto bekommt einen Vorschau-Button', async () => {
  const page = await loadPage();
  const indexes = buttons(page).map((b) => b.dataset.index);
  assert.deepEqual(indexes, ['0', '1', '2'], 'die Vorschau-Buttons haben die data-index-Werte ' + JSON.stringify(indexes));
  const types = buttons(page).map((b) => b.getAttribute('type'));
  assert.deepEqual(types, ['button', 'button', 'button'], 'die Vorschau-Buttons haben die Typen ' + JSON.stringify(types));
});

test('Jedes Vorschaubild zeigt sein Foto mit alt-Text', async () => {
  const page = await loadPage();
  const alts = images(page).map((img) => img.getAttribute('alt'));
  assert.deepEqual(
    alts,
    ['A red fox in the snow', 'An owl on a branch', 'A heron by the river'],
    'die alt-Texte der Vorschaubilder sind ' + JSON.stringify(alts)
  );
  const data = images(page).map((img) => String(img.getAttribute('src')).startsWith('data:image/svg+xml,'));
  assert.deepEqual(data, [true, true, true], 'eine data:-URL als src, je Vorschaubild: ' + JSON.stringify(data));
});

test('Das Hauptbild zeigt das erste Foto', async () => {
  const page = await loadPage();
  const main = page.document.querySelector('#main');
  const same = main.getAttribute('src') === images(page)[0].getAttribute('src');
  assert.equal(same, true, 'das Hauptbild hat nicht das src des ersten Vorschaubilds');
  const shown = main.getAttribute('alt');
  assert.equal(shown, 'A red fox in the snow', 'das Hauptbild hat den alt-Text ' + JSON.stringify(shown));
});

test('Die Bildunterschrift zeigt den alt-Text', async () => {
  const page = await loadPage();
  const text = page.document.querySelector('#caption').textContent;
  assert.equal(text, 'A red fox in the snow', 'die Bildunterschrift zeigt ' + JSON.stringify(text));
});

test('Nur das erste Vorschaubild ist als aktuell markiert', async () => {
  const page = await loadPage();
  const marks = buttons(page).map((b) => b.classList.contains('selected') + ' ' + b.getAttribute('aria-current'));
  assert.deepEqual(marks, ['true true', 'false null', 'false null'], 'Klasse selected und aria-current je Vorschaubild: ' + JSON.stringify(marks));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery</title>
</head>
<body>
  <h1>Gallery</h1>
  <figure>
    <img id="main" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22/%3E" alt="">
    <figcaption id="caption"></figcaption>
  </figure>
  <button id="prev" type="button">Previous</button>
  <button id="next" type="button">Next</button>
  <ul id="thumbs"></ul>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Übung 2 von 3

Ein Foto mit einem Listener wählen

Ein Klick auf ein Vorschaubild, auch auf das Bild darin, soll dieses Foto aktuell machen und render() aufrufen: Hauptbild, Bildunterschrift und markiertes Vorschaubild folgen. Ersetzen Sie die Schleife des Starters durch einen einzigen Klick-Listener auf #thumbs, der closest benutzt und contains prüft. Ein Klick auf die Liste zwischen den Vorschaubildern ändert nichts. Der Starter fügt jedem Button einmal einen Listener hinzu; render() ersetzt die Buttons danach, also bewirkt der zweite Klick nichts.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der erste Klick funktioniert, der zweite bewirkt nichts, weil render() neue Buttons ohne Listener gebaut hat.

  2. Hinweis 2

    Löschen Sie die Schleife. Fügen Sie thumbs.addEventListener('click', (event) => { … }) einmal hinzu, außerhalb von render().

  3. Hinweis 3

    Darin: const button = event.target.closest('button'); return, wenn !button || !thumbs.contains(button); dann current = Number(button.dataset.index) und render().

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

// One listener for all thumbnails, also the ones render() builds later.
thumbs.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !thumbs.contains(button)) return;
  current = Number(button.dataset.index);
  render();
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

render();

// TODO: one listener on #thumbs instead of one per button.
for (const button of thumbs.querySelectorAll('button')) {
  button.addEventListener('click', () => {
    current = Number(button.dataset.index);
    render();
  });
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const thumb = (page, n) => page.document.querySelectorAll('#thumbs img')[n];
const caption = (page) => page.document.querySelector('#caption').textContent;
const current = (page) => [...page.document.querySelectorAll('#thumbs button')].map((b) => b.getAttribute('aria-current') === 'true');

test('Ein Klick auf ein Vorschaubild zeigt dieses Foto', async () => {
  const page = await loadPage();
  thumb(page, 1).click();
  const same = page.document.querySelector('#main').getAttribute('src') === thumb(page, 1).getAttribute('src');
  assert.equal(same, true, 'nach dem Klick auf das zweite Vorschaubild hat das Hauptbild nicht dessen src');
  assert.equal(caption(page), 'An owl on a branch', 'nach dem Klick auf das zweite Vorschaubild ist die Bildunterschrift ' + JSON.stringify(caption(page)));
});

test('Das geklickte Vorschaubild wird das einzige aktuelle', async () => {
  const page = await loadPage();
  thumb(page, 2).click();
  assert.deepEqual(current(page), [false, false, true], 'nach dem Klick auf das dritte Vorschaubild sitzt aria-current auf ' + JSON.stringify(current(page)));
  const selected = page.document.querySelectorAll('#thumbs .selected').length;
  assert.equal(selected, 1, selected + ' Vorschaubilder haben die Klasse selected');
});

test('Auch ein zweiter Klick auf ein anderes Vorschaubild funktioniert', async () => {
  const page = await loadPage();
  thumb(page, 1).click();
  thumb(page, 2).click();
  assert.equal(caption(page), 'A heron by the river', 'nach Klicks auf das zweite, dann das dritte Vorschaubild ist die Bildunterschrift ' + JSON.stringify(caption(page)));
});

test('Ein Klick auf die Liste zwischen den Vorschaubildern ändert nichts', async () => {
  const page = await loadPage();
  page.document.querySelector('#thumbs').click();
  assert.equal(caption(page), 'A red fox in the snow', 'nach einem Klick auf die Liste ist die Bildunterschrift ' + JSON.stringify(caption(page)));
  assert.deepEqual(current(page), [true, false, false], 'nach einem Klick auf die Liste sitzt aria-current auf ' + JSON.stringify(current(page)));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery</title>
</head>
<body>
  <h1>Gallery</h1>
  <figure>
    <img id="main" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22/%3E" alt="">
    <figcaption id="caption"></figcaption>
  </figure>
  <button id="prev" type="button">Previous</button>
  <button id="next" type="button">Next</button>
  <ul id="thumbs"></ul>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Übung 3 von 3

Zurück, Weiter und die Pfeiltasten

Schreiben Sie show(index): Die Funktion bringt index in den Bereich 0 bis photos.length - 1, macht ihn aktuell und ruft render() auf. Weiter zeigt das nächste Foto und geht vom letzten zurück zum ersten; Zurück zeigt das vorige und geht vom ersten zum letzten. Ein keydown-Listener auf document tut dasselbe für ArrowRight und ArrowLeft und ignoriert andere Tasten. Weiter im Starter läuft über das letzte Foto hinaus, wo photos[3] undefined ist und render() einen Fehler wirft; Zurück und die Tasten bewirken nichts.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Das dritte Weiter wirft einen TypeError zu src, und Zurück und die Tasten ändern nichts.

  2. Hinweis 2

    show(index) setzt current = (index + photos.length) % photos.length und ruft render() auf. Die vorher addierte Länge verhindert, dass -1 negativ bleibt.

  3. Hinweis 3

    Rufen Sie show(current + 1) und show(current - 1) von den beiden Buttons auf, und aus document.addEventListener('keydown', …), wenn event.key 'ArrowRight' oder 'ArrowLeft' ist.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

// One listener for all thumbnails, also the ones render() builds later.
thumbs.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !thumbs.contains(button)) return;
  current = Number(button.dataset.index);
  render();
});

function show(index) {
  current = (index + photos.length) % photos.length; // wrap around at both ends
  render();
}

document.querySelector('#prev').addEventListener('click', () => show(current - 1));
document.querySelector('#next').addEventListener('click', () => show(current + 1));
document.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowLeft') show(current - 1);
  if (event.key === 'ArrowRight') show(current + 1);
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Each picture is a small SVG drawing inside a data: URL: no image files to load.
const picture = (colour, label) =>
  'data:image/svg+xml,' +
  encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120">' +
      '<rect width="160" height="120" fill="' + colour + '"/>' +
      '<text x="80" y="66" font-size="20" text-anchor="middle" fill="white">' + label + '</text></svg>'
  );

const photos = [
  { src: picture('sienna', 'Fox'), alt: 'A red fox in the snow' },
  { src: picture('slategray', 'Owl'), alt: 'An owl on a branch' },
  { src: picture('steelblue', 'Heron'), alt: 'A heron by the river' }
];
let current = 0;

const thumbs = document.querySelector('#thumbs');
const mainImage = document.querySelector('#main');
const caption = document.querySelector('#caption');

function render() {
  const items = photos.map((photo, index) => {
    const img = document.createElement('img');
    img.setAttribute('src', photo.src);
    img.setAttribute('alt', photo.alt);
    const button = document.createElement('button');
    button.type = 'button';
    button.dataset.index = String(index);
    if (index === current) {
      button.classList.add('selected');
      button.setAttribute('aria-current', 'true');
    }
    button.append(img);
    const li = document.createElement('li');
    li.append(button);
    return li;
  });
  thumbs.replaceChildren(...items);
  mainImage.setAttribute('src', photos[current].src);
  mainImage.setAttribute('alt', photos[current].alt);
  caption.textContent = photos[current].alt;
}

// One listener for all thumbnails, also the ones render() builds later.
thumbs.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !thumbs.contains(button)) return;
  current = Number(button.dataset.index);
  render();
});

// TODO: wrap around at both ends, add Previous, and the arrow keys.
document.querySelector('#next').addEventListener('click', () => {
  current += 1;
  render();
});

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const shown = (page) => page.document.querySelector('#main').getAttribute('alt');
const press = (page, key) => page.document.body.dispatchEvent(new page.window.KeyboardEvent('keydown', { key, bubbles: true }));
const next = (page) => page.document.querySelector('#next').click();
const prev = (page) => page.document.querySelector('#prev').click();

test('Weiter zeigt das nächste Foto', async () => {
  const page = await loadPage();
  next(page);
  assert.equal(shown(page), 'An owl on a branch', 'nach einmal Weiter ist das Hauptbild ' + JSON.stringify(shown(page)));
});

test('Weiter auf dem letzten Foto geht zurück zum ersten', async () => {
  const page = await loadPage();
  next(page);
  next(page);
  next(page);
  assert.equal(shown(page), 'A red fox in the snow', 'nach dreimal Weiter ist das Hauptbild ' + JSON.stringify(shown(page)));
});

test('Zurück auf dem ersten Foto geht zum letzten', async () => {
  const page = await loadPage();
  prev(page);
  assert.equal(shown(page), 'A heron by the river', 'nach Zurück auf dem ersten Foto ist das Hauptbild ' + JSON.stringify(shown(page)));
});

test('ArrowRight und ArrowLeft blättern durch die Fotos', async () => {
  const page = await loadPage();
  press(page, 'ArrowRight');
  assert.equal(shown(page), 'An owl on a branch', 'nach ArrowRight ist das Hauptbild ' + JSON.stringify(shown(page)));
  press(page, 'ArrowLeft');
  press(page, 'ArrowLeft');
  assert.equal(shown(page), 'A heron by the river', 'nach ArrowRight und zweimal ArrowLeft ist das Hauptbild ' + JSON.stringify(shown(page)));
});

test('Andere Tasten ändern nichts', async () => {
  const page = await loadPage();
  press(page, 'Enter');
  press(page, 'a');
  assert.equal(shown(page), 'A red fox in the snow', 'nach Enter und a ist das Hauptbild ' + JSON.stringify(shown(page)));
});

test('Das markierte Vorschaubild folgt Zurück', async () => {
  const page = await loadPage();
  prev(page);
  const marked = [...page.document.querySelectorAll('#thumbs button')].map((b) => b.getAttribute('aria-current') === 'true');
  assert.deepEqual(marked, [false, false, true], 'nach Zurück sitzt aria-current auf ' + JSON.stringify(marked));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery</title>
</head>
<body>
  <h1>Gallery</h1>
  <figure>
    <img id="main" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22/%3E" alt="">
    <figcaption id="caption"></figcaption>
  </figure>
  <button id="prev" type="button">Previous</button>
  <button id="next" type="button">Next</button>
  <ul id="thumbs"></ul>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Häufige Fehler

Über das letzte Foto hinaus blättern, ohne umzulaufen

// Only the alt texts here, to keep the example short.
const photos = [
  { alt: 'A red fox in the snow' },
  { alt: 'An owl on a branch' }
];
let current = 0;
const caption = document.querySelector('#caption');

function next() {
  current += 1;
  caption.textContent = photos[current].alt;
}
next(); // the owl
next(); // one past the end

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'alt')

Warum, und die Lösung

Bei zwei Fotos sind die Indizes 0 und 1. Das zweite next() macht current zu 2, und photos[2] ist undefined, also scheitert das Lesen von .alt. Lassen Sie den Index umlaufen: current = (current + 1) % photos.length. Firefox und Safari formulieren diese Meldung anders.

forEach auf element.children aufrufen

const thumbs = document.querySelector('#thumbs');
// Unmark every thumbnail before marking the new one:
thumbs.children.forEach((li) => li.classList.remove('selected'));

Was Node.js ausgibt

TypeError: thumbs.children.forEach is not a function

Warum, und die Lösung

element.children ist eine HTMLCollection, und die hat kein forEach. Machen Sie zuerst ein Array daraus, [...thumbs.children].forEach(…), oder nehmen Sie for...of. In der Galerie brauchen Sie beides nicht: render() baut jedes Vorschaubild neu und markiert nur das aktuelle.

setAttribute nur mit dem Namen aufrufen

const button = document.querySelector('#thumbs button');
// Mark the thumbnail as the current one:
button.setAttribute('aria-current');

Was Node.js ausgibt

TypeError: Failed to execute 'setAttribute' on 'Element': 2 arguments required, but only 1 present.

Warum, und die Lösung

setAttribute nimmt immer einen Namen und einen Wert. Für aria-current ist der Wert "true": button.setAttribute('aria-current', 'true'). Ein leerer Wert zählt nicht: Laut MDN wird ein leeres aria-current dem Nutzer nicht mitgeteilt.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Erst die Daten, dann render()

Die Galerie ist ein Array von Fotos, jedes { src, alt }, und eine Zahl, current, der Index des gezeigten Fotos. render() baut die Seite daraus: Jedes Foto wird ein li mit einem Button, der ein kleines img enthält, und replaceChildren setzt sie ein, ein erneutes render() verdoppelt also nichts. Das Hauptbild bekommt src und alt des aktuellen Fotos per setAttribute, die Bildunterschrift zeigt den alt-Text per textContent. Bilddateien gibt es nicht: Jedes src ist eine data:-URL, die ein kleines SVG-Bild in der URL selbst trägt, erzeugt von der Hilfsfunktion picture(). Alt-Text ist keine Dekoration: Ein Screenreader liest ihn vor, und der Browser zeigt ihn, wenn ein Bild nicht lädt.

Ein Listener für die Vorschaubilder

render() ersetzt die Buttons jedes Mal, Listener auf den alten Buttons sind nach dem ersten Klick also weg. Setzen Sie stattdessen einen Listener auf #thumbs. event.target kann das img im Button sein, finden Sie den Button daher mit event.target.closest("button"), kehren Sie früh zurück, wenn er null ist oder nicht in der Liste liegt, und lesen Sie sein data-index. dataset-Werte sind Strings: Wandeln Sie sie mit Number() um, bevor Sie rechnen. Markieren Sie das aktuelle Vorschaubild doppelt: mit einer Klasse wie selected für das Aussehen und mit aria-current="true", damit Screenreader es ansagen. MDN verlangt, in einer Menge nur ein Element als aktuell zu markieren, und render() stellt das sicher.

Umlaufen am Ende

Weiter nach dem letzten Foto soll das erste zeigen, Zurück vor dem ersten das letzte. Der Restoperator leistet das: (current + 1) % photos.length ist nach dem letzten Index 0. Rückwärts ist (current - 1) % 3 gleich -1, wenn current 0 ist, denn % behält das Vorzeichen der linken Zahl; addieren Sie zuerst die Länge: (current - 1 + photos.length) % photos.length. Legen Sie die Formel in eine Funktion, show(index), und rufen Sie sie von den Buttons und den Tasten aus auf. Für die Tasten hören Sie auf keydown an document: Tastendrücke steigen vom Element mit dem Fokus zu ihm auf. event.key ist "ArrowLeft" oder "ArrowRight"; alle anderen Tasten ignorieren Sie.

Quellen

Zuletzt geprüft am 4. Oktober 2026