Przejdź do treści

Temat 17 · 2 × 45 minINF.03

Stan aplikacji: zmienne globalne, indeksy, zawijanie

Kliknięcie trwa ułamek sekundy, ale strona musi pamiętać, co się stało: które zdjęcie galerii jest widoczne, ile razy kliknięto, która zakładka jest otwarta. Ta pamięć to stan aplikacji (state). W tej lekcji poznasz najprostszy – i na egzaminie w zupełności wystarczający – sposób jej przechowywania: zmienną zadeklarowaną poza funkcją. Na niej zbudujesz wzorce, które od 2024 r. wracają w arkuszach INF.03: licznik z limitem, galerię „<” / „>” z zawijaniem indeksu, rotację cytatów i obliczenia zależne od tego, co aktualnie widać na stronie.

Dlaczego funkcja niczego nie pamięta

Zmienna zadeklarowana wewnątrz funkcji jest lokalna: powstaje przy każdym wywołaniu od nowa i znika, gdy funkcja się kończy. Jeśli licznik kliknięć zadeklarujesz w środku funkcji obsługującej kliknięcie, każde kliknięcie zaczyna liczyć od zera.

Sprawdź się

Co wypisze ten kod?

function dodaj() {
let licznik = 0;
licznik++;
console.log(licznik);
}
dodaj();
dodaj();
dodaj();
licznik-lokalny.js
function dodaj() {
  let licznik = 0; // powstaje od nowa przy KAŻDYM wywołaniu
  licznik++;
  console.log('lokalny:', licznik);
}
dodaj();
dodaj();
dodaj();

let liczbaKlikniec = 0; // zadeklarowane RAZ, poza funkcją
function dodajPoprawnie() {
  liczbaKlikniec++; // funkcja tylko zmienia istniejącą zmienną
  console.log('globalny:', liczbaKlikniec);
}
dodajPoprawnie();
dodajPoprawnie();
dodajPoprawnie();
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zasada jest prosta: stan deklarujesz raz, poza funkcją, a funkcje go tylko odczytują i zmieniają. Taką zmienną nazywamy globalną (jest widoczna w całym skrypcie). W dużych aplikacjach zmiennych globalnych się unika, ale w skrypcie egzaminacyjnym – kilkadziesiąt linii, jeden plik – to najprostsze i całkowicie poprawne rozwiązanie.

Schemat: zdarzenie uruchamia funkcję, funkcja odczytuje i zmienia zmienną stanu zadeklarowaną poza nią, a następnie aktualizuje DOM
Rys. 1. Cykl stanu: zdarzenie → funkcja → zmiana stanu → aktualizacja DOM. Stan żyje między kliknięciami, zmienne lokalne nie.

Do stanu, który się zmienia, używaj let. Do odnośników do elementów strony (nie zmieniają się) – const. W „dialekcie egzaminacyjnym” zobaczysz var – dla naszych celów działa tak samo.

Licznik kliknięć z resetem
let liczbaKlikniec = 0; // stan – poza funkcjami

const wynik = document.getElementById('wynik');

function zwieksz() {
  liczbaKlikniec++;
  wynik.textContent = liczbaKlikniec; // każda zmiana stanu → odświeżenie DOM
}

function wyzeruj() {
  liczbaKlikniec = 0;
  wynik.textContent = liczbaKlikniec;
}

document.getElementById('plus').addEventListener('click', zwieksz);
document.getElementById('reset').addEventListener('click', wyzeruj);
Podgląd

Licznik z granicą

Druga najczęstsza potrzeba: wartość ma rosnąć lub maleć, ale nie może przekroczyć limitu. W arkuszu z czerwca 2024 (INF.03-08-24.06) pasek postępu rósł o 12 % przy każdym opuszczeniu pola, a zadanie wymagało zabezpieczenia funkcji tak, by szerokość nigdy nie przekroczyła 100 %. Masz dwa równoważne zapisy:

limit.js
let postep = 0;

// Wersja 1: if po zmianie
function zwiekszIf() {
  postep += 12;
  if (postep > 100) postep = 100;
}

// Wersja 2: Math.min wybiera mniejszą z dwu liczb
function zwiekszMin() {
  postep = Math.min(postep + 12, 100);
}

for (let i = 0; i < 9; i++) zwiekszMin();
console.log('po 9 krokach:', postep); // 100, nie 108

// analogicznie dolna granica: Math.max(wartosc - 1, 0)
let temperatura = 2;
temperatura = Math.max(temperatura - 5, 0);
console.log('temperatura:', temperatura); // 0
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Pasek startuje od 16 %, każde zdarzenie dodaje 12 % z limitem 100 %. Jaką szerokość ma pasek po 7 zdarzeniach?

Pasek postępu rosnący przy opuszczeniu pola
let szerokosc = 16; // stan początkowy zgodny z CSS
const pasek = document.getElementById('pasek');

function zwiekszPostep() {
  szerokosc = Math.min(szerokosc + 12, 100); // nigdy ponad 100
  pasek.style.width = szerokosc + '%';
  pasek.textContent = szerokosc + '%';
}

// jedno nasłuchiwanie dla każdego pola – zdarzenie blur = opuszczenie pola
const pola = document.querySelectorAll('input');
for (const pole of pola) {
  pole.addEventListener('blur', zwiekszPostep);
}
Podgląd

Zwróć uwagę na dwie rzeczy: stan początkowy zmiennej (16) zgadza się z tym, co widać w CSS, a jedna funkcja obsługuje wszystkie pola, bo pętla podpina ją do każdego z nich.

Indeks i zawijanie: galeria „<” i „>”

Galeria to licznik, który po ostatnim elemencie wraca do pierwszego, a przed pierwszym skacze na ostatni. Arkusz INF.03-09-24.06 opisywał to wprost: zdjęcia 1.jpg…7.jpg, po 7.jpg ma pokazać się 1.jpg, przed 1.jpg – 7.jpg. Zawijanie (wrap-around) zapiszesz na dwa sposoby.

Sposób 1: if po zmianie. Czytelny, działa dla numeracji od 1 (jak nazwy plików):

zawijanie-if.js
const LICZBA_ZDJEC = 7;
let numer = 1; // numer pliku: 1…7

function nastepne() {
  numer++;
  if (numer > LICZBA_ZDJEC) numer = 1; // po 7 wracamy na 1
}
function poprzednie() {
  numer--;
  if (numer < 1) numer = LICZBA_ZDJEC; // przed 1 skaczemy na 7
}

for (let i = 0; i < 8; i++) nastepne();
console.log('po 8 × „>” od 1:', numer); // 2
poprzednie(); poprzednie();
console.log('po 2 × „<”:', numer); // 7
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Sposób 2: operator % (reszta z dzielenia). Najkrótszy, ale wymaga numeracji od 0 (indeksy tablicy) i uwagi przy cofaniu:

zawijanie-modulo.js
const zdjecia = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg'];
let indeks = 0; // indeks tablicy: 0…6

function nastepne() {
  indeks = (indeks + 1) % zdjecia.length; // 6 + 1 = 7, 7 % 7 = 0
}
function poprzednie() {
  // UWAGA: (0 - 1) % 7 daje -1, nie 6! Dodaj długość przed dzieleniem:
  indeks = (indeks - 1 + zdjecia.length) % zdjecia.length;
}

console.log((0 - 1) % 7);         // -1  ← pułapka
console.log((0 - 1 + 7) % 7);     // 6   ← poprawnie
poprzednie();
console.log('plik:', zdjecia[indeks]); // 7.jpg
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Galeria ma 7 zdjęć, zmienna numer startuje od 1 i jest zawijana. Jaką wartość ma numer po ośmiu kliknięciach „>” i jednym kliknięciu „<”?

Sprawdź się

Co wypisze console.log((0 - 1) % 5);?

Teraz pełna galeria. W zadaniu egzaminacyjnym dostaniesz pliki 1.jpg…7.jpg; tutaj plac zabaw nie ma plików, więc obrazki rysujemy w locie jako SVG – ale nazwa pliku nadal trafia do src, więc kod galerii wygląda identycznie jak na egzaminie.

Galeria z zawijaniem w obie strony
// Na egzaminie: obraz.src = numer + '.jpg'. Tu generujemy obrazek, bo plac zabaw nie ma plików.
function adresObrazka(numer) {
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="260" height="140">' +
    '<rect width="260" height="140" fill="hsl(' + numer * 47 + ' 60% 45%)"/>' +
    '<text x="130" y="85" font-size="36" fill="white" text-anchor="middle" font-family="system-ui">' +
    numer + '.jpg</text></svg>';
  return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}

const LICZBA_ZDJEC = 7;
let numer = 1; // stan galerii

const zdjecie = document.getElementById('zdjecie');
const licznik = document.getElementById('licznik');

function odswiez() {
  zdjecie.src = adresObrazka(numer);   // na egzaminie: numer + '.jpg'
  zdjecie.alt = 'Zdjęcie ' + numer;
  licznik.textContent = numer + ' / ' + LICZBA_ZDJEC;
}

function nastepne() {
  numer++;
  if (numer > LICZBA_ZDJEC) numer = 1;
  odswiez();
}

function poprzednie() {
  numer--;
  if (numer < 1) numer = LICZBA_ZDJEC;
  odswiez();
}

document.getElementById('dalej').addEventListener('click', nastepne);
document.getElementById('wstecz').addEventListener('click', poprzednie);
odswiez(); // stan początkowy też przechodzi przez tę samą funkcję
Podgląd

Rotacja: 1 → 2 → 3 → 1

Odmiana zawijania bez przycisków: kliknięcie w widoczny element ukrywa go i pokazuje następny, a po ostatnim wraca pierwszy (INF.03-10-24.06: cytaty pracowników). Arkusz dopuszczał „jedną funkcję parametrową lub trzy osobne”. Trzy osobne funkcje są dozwolone, ale jedna z parametrem to mniej kodu i mniej miejsc na literówkę.

Rotacja cytatów – kliknij cytat
const LICZBA_CYTATOW = 3;

// Styl egzaminacyjny: atrybut onclick="pokazNastepny(2)" w HTML wywołuje tę funkcję
function pokazNastepny(numerKlikniety) {
  let nastepny = numerKlikniety + 1;
  if (nastepny > LICZBA_CYTATOW) nastepny = 1; // po 3 wraca 1

  document.getElementById('cytat' + numerKlikniety).style.display = 'none';
  document.getElementById('cytat' + nastepny).style.display = 'block';
}
Podgląd

Zauważ sztuczkę 'cytat' + numer: identyfikatory z numerem pozwalają jednej funkcji dotrzeć do dowolnego elementu. Tu stan nie jest nawet w zmiennej – „pamięta” go DOM (to, który cytat jest widoczny), a funkcja dostaje go w parametrze.

Sprawdź się

W powyższym kodzie użytkownik widzi cytat 3 i klika go. Który cytat zobaczy?

Jedna funkcja dla wielu przycisków

Zakładki, przyciski nawigacji, miniatury – to zawsze ten sam mechanizm: najpierw wszystkim elementom ustaw stan „nieaktywny”, potem jednemu „aktywny”. Jeśli pominiesz pierwszy krok, po trzech kliknięciach wszystkie zakładki będą widoczne jednocześnie. Funkcja musi wiedzieć, który przycisk kliknięto. Są trzy sposoby przekazania tej informacji.

SposóbHTMLJSKiedy
parametr w onclickonclick="pokaz('adres')"function pokaz(nazwa) {…}styl arkuszy CKE; prosty i czytelny
this w onclickonclick="aktywuj(this)"function aktywuj(przycisk) {…}gdy potrzebujesz samego klikniętego elementu
event.targetbrak atrybutówaddEventListener('click', e => … e.target …)gdy przyciski podpinasz pętlą
Zakładki jedną funkcją z parametrem
function pokazKarte(idKarty, klikniety) {
  // 1. wszystkim „nieaktywny”
  const karty = document.querySelectorAll('.karta');
  for (const karta of karty) karta.style.display = 'none';

  const przyciski = document.querySelectorAll('.karta-btn');
  for (const przycisk of przyciski) przycisk.style.backgroundColor = 'Crimson';

  // 2. jednemu „aktywny”
  document.getElementById(idKarty).style.display = 'block';
  klikniety.style.backgroundColor = 'Salmon';
}

// Stan początkowy: pierwszy przycisk podświetlony tak samo, jak po kliknięciu
document.querySelector('.karta-btn').style.backgroundColor = 'Salmon';
Podgląd

Ta sama logika z addEventListener i event.target – bez atrybutów onclick w HTML. Każdy przycisk zna swoją kartę przez atrybut data-karta, który odczytasz jako przycisk.dataset.karta:

Zakładki z addEventListener i data-atrybutem
const przyciski = document.querySelectorAll('#menu button');

function przelaczKarte(zdarzenie) {
  const klikniety = zdarzenie.target;            // element, który kliknięto
  const idKarty = klikniety.dataset.karta;       // z atrybutu data-karta

  for (const p of przyciski) p.classList.remove('aktywny');
  for (const k of document.querySelectorAll('.karta')) k.classList.remove('aktywna');

  klikniety.classList.add('aktywny');
  document.getElementById(idKarty).classList.add('aktywna');
}

for (const przycisk of przyciski) {
  przycisk.addEventListener('click', przelaczKarte); // ta sama funkcja dla każdego
}
przyciski[0].classList.add('aktywny'); // stan początkowy
Podgląd

Odczyt stanu z DOM

Nie każdy stan trzeba trzymać w zmiennej. Czasem wygodniej zapytać stronę, co aktualnie pokazuje. Arkusz INF.03-03-26.01 wymagał: miniatury zmieniają duży obraz (trójkąt albo prostokąt), a przycisk „Oblicz” liczy pole tej figury, która jest teraz wyświetlona – „obliczenia powinny też działać dla stanu początkowego witryny”. Masz kilka źródeł prawdy:

Co sprawdzaszKodUwaga
nazwa pliku w srcobraz.src.includes('trojkat')src zwraca pełny adres, dlatego includes, nie ===
tekst alternatywnyobraz.alt === 'trójkąt'ustawiaj alt razem ze src
atrybut data-*obraz.dataset.figura === 'trojkat'najczytelniejsze; w HTML data-figura="trojkat"
klasaelement.classList.contains('aktywny')dobre dla zakładek i przycisków
widocznośćgetComputedStyle(el).display === 'none'patrz pułapka niżej
Pole figury zależne od wyświetlonego obrazu
// Na egzaminie: pliki 1m.bmp / 2m.bmp (miniatury) i 1d.bmp / 2d.bmp (duże). Tu rysujemy figury w SVG.
function rysunek(figura, w, h) {
  const ksztalt = figura === 'trojkat'
    ? '<polygon points="' + (w / 2) + ',5 5,' + (h - 5) + ' ' + (w - 5) + ',' + (h - 5) + '" fill="SteelBlue"/>'
    : '<rect x="5" y="5" width="' + (w - 10) + '" height="' + (h - 10) + '" fill="DarkOrange"/>';
  return 'data:image/svg+xml;utf8,' + encodeURIComponent(
    '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '">' + ksztalt + '</svg>');
}

const duzy = document.getElementById('duzy');
const miniatury = document.querySelectorAll('.miniatura');
for (const m of miniatury) m.src = rysunek(m.dataset.figura, 60, 40);
duzy.src = rysunek('trojkat', 180, 120); // stan początkowy: trójkąt

function wybierzFigure(miniatura) {
  const figura = miniatura.dataset.figura;
  duzy.src = rysunek(figura, 180, 120);   // na egzaminie: duzy.src = '2d.bmp'
  duzy.dataset.figura = figura;           // zapisujemy stan w DOM
  duzy.alt = miniatura.alt;
}

function obliczPole() {
  const a = parseFloat(document.getElementById('bokA').value);
  const b = parseFloat(document.getElementById('bokB').value);
  let pole;
  // odczyt stanu: która figura jest TERAZ wyświetlona?
  if (duzy.dataset.figura === 'trojkat') {
    pole = 0.5 * a * b;
  } else {
    pole = a * b;
  }
  document.getElementById('wynik').textContent =
    'Pole ' + duzy.alt + 'a: ' + pole; // np. „Pole trójkąta: 6”
}
Podgląd

Gdybyś na egzaminie miał tylko pliki 1d.bmp i 2d.bmp, warunek wyglądałby tak: if (duzy.src.includes('1d')). Działa, bo src zawsze zawiera nazwę pliku – ale zwraca pełny adres (file:///C:/…/1d.bmp), więc porównanie duzy.src === '1d.bmp' byłoby zawsze fałszywe. Bezpieczniej: duzy.getAttribute('src') === '1d.bmp' zwraca dokładnie to, co wpisano w HTML.

style-vs-computed.js
// zakładamy: w CSS jest  #panel { display: none; }
const panel = document.getElementById('panel');
console.log(panel.style.display);                 // ""  – pusto! styl jest w CSS, nie w atrybucie
console.log(getComputedStyle(panel).display);     // "none" – faktyczna wartość

// przełącznik pokaż/ukryj, który działa od pierwszego kliknięcia:
function przelacz() {
  const ukryty = getComputedStyle(panel).display === 'none';
  panel.style.display = ukryty ? 'block' : 'none';
}

Stan w tablicy

Gdy elementów jest więcej albo mają treść (cytaty, podpisy, ceny), trzymaj je w tablicy, a stanem jest indeks. Zmiana stanu to zmiana indeksu, a odświeżenie DOM to odczyt tablica[indeks].

Cytat dnia z tablicy: następny i losowy
const cytaty = [
  'Najpierw spraw, by działało. Potem, by było ładne.',
  'Każdy błąd w konsoli to wskazówka, nie wyrok.',
  'Testuj na dwóch zestawach danych.',
  'Nazwa zmiennej to komentarz, którego nie da się pominąć.'
];
let indeks = 0; // stan: który cytat jest widoczny

const cytat = document.getElementById('cytat');
const info = document.getElementById('info');

function odswiez() {
  cytat.textContent = cytaty[indeks];
  info.textContent = 'Cytat ' + (indeks + 1) + ' z ' + cytaty.length; // dla człowieka numerujemy od 1
}

document.getElementById('nastepny').addEventListener('click', () => {
  indeks = (indeks + 1) % cytaty.length; // zawijanie przez modulo
  odswiez();
});

document.getElementById('losowy').addEventListener('click', () => {
  indeks = Math.floor(Math.random() * cytaty.length); // 0 … length-1
  odswiez();
});

odswiez();
Podgląd
Sprawdź się

Tablica ma 4 elementy, indeks wynosi 3. Co da indeks = (indeks + 1) % 4 i co pokaże cytaty[indeks]?

Na egzaminie

Ściąga

SkładniaCo robiPrzykład
let stan = 0; poza funkcjązmienna żyje między kliknięciamilet numer = 1;
stan = Math.min(stan + k, max)wzrost z górnym limitempostep = Math.min(postep + 12, 100)
stan = Math.max(stan - k, min)spadek z dolnym limitemilosc = Math.max(ilosc - 1, 0)
i++; if (i > n) i = 1;zawijanie w przód (numeracja od 1)galeria 1…7
i--; if (i < 1) i = n;zawijanie w tył (numeracja od 1)galeria 1…7
i = (i + 1) % nzawijanie w przód (indeksy 0…n−1)indeks = (indeks + 1) % tab.length
i = (i - 1 + n) % nzawijanie w tył bez ujemnych(0 - 1 + 7) % 7 === 6
'cytat' + numerid z numerem – jedna funkcja, wiele elementówgetElementById('cytat' + 2)
onclick="f('adres', this)"parametr i kliknięty elementfunction f(id, przycisk)
e.target, e.target.dataset.xkto wywołał zdarzenie, atrybut data-xaddEventListener('click', e => …)
img.src.includes('1d')odczyt stanu z adresu obrazusrc to pełny adres – nie porównuj ===
el.getAttribute('src')dokładnie to, co w HTML=== '1d.bmp' działa
el.classList.contains('aktywny')odczyt stanu z klasyzakładki, przyciski
getComputedStyle(el).displayfaktyczna widoczność (także z CSS)el.style.display bywa puste
Math.floor(Math.random() * n)losowy indeks 0…n−1losowy cytat

Zadania

Zadanie 1
łatwe

Licznik biletów 0–10

Zbuduj licznik liczby biletów z przyciskami „−” i „+” oraz akapitem <p id="ilosc">0</p>. Wymagania:

  • wartość nie spada poniżej 0 i nie rośnie powyżej 10,
  • gdy osiągnie granicę, odpowiedni przycisk zostaje wyłączony (disabled = true), a po odejściu od granicy – włączony,
  • pod licznikiem akapit <p id="cena"></p> pokazuje „Do zapłaty: X zł” przy cenie 24 zł za bilet.
Pokaż rozwiązanie
Rozwiązanie: licznik biletów
const CENA_BILETU = 24;
const MAKSIMUM = 10;
let liczbaBiletow = 0; // stan

const minus = document.getElementById('minus');
const plus = document.getElementById('plus');

function odswiez() {
  document.getElementById('ilosc').textContent = liczbaBiletow;
  document.getElementById('cena').textContent = 'Do zapłaty: ' + liczbaBiletow * CENA_BILETU + ' zł';
  minus.disabled = liczbaBiletow === 0;         // na dolnej granicy wyłącz „−”
  plus.disabled = liczbaBiletow === MAKSIMUM;   // na górnej wyłącz „+”
}

plus.addEventListener('click', () => {
  liczbaBiletow = Math.min(liczbaBiletow + 1, MAKSIMUM);
  odswiez();
});
minus.addEventListener('click', () => {
  liczbaBiletow = Math.max(liczbaBiletow - 1, 0);
  odswiez();
});

odswiez(); // stan początkowy: „−” od razu wyłączony
Podgląd
Zadanie 2
średnie

Karuzela opinii jedną funkcją

Na stronie są cztery bloki <div class="opinia" id="opinia1">…id="opinia4", każdy z atrybutem onclick="nastepnaOpinia(N)", gdzie N to numer bloku. Na start widoczny jest tylko opinia1 (pozostałe ukryte w CSS). Napisz jedną funkcję nastepnaOpinia(numer), która ukrywa kliknięty blok i pokazuje następny; po czwartym ma wrócić pierwszy. Dodaj przycisk „Pokaż wszystkie”, który wyświetla wszystkie cztery bloki naraz.

Pokaż rozwiązanie
Rozwiązanie: karuzela opinii
const LICZBA_OPINII = 4;

function nastepnaOpinia(numer) {
  let nastepny = numer + 1;
  if (nastepny > LICZBA_OPINII) nastepny = 1; // zawijanie 4 → 1
  document.getElementById('opinia' + numer).style.display = 'none';
  document.getElementById('opinia' + nastepny).style.display = 'block';
}

function pokazWszystkie() {
  const opinie = document.querySelectorAll('.opinia');
  for (const opinia of opinie) opinia.style.display = 'block';
}
Podgląd
Zadanie 3
trudne

Galeria z kropkami i klawiaturą

Rozbuduj galerię z lekcji: tablica zdjecia ma 5 elementów (użyj funkcji adresObrazka z lekcji albo własnych plików). Wymagania:

  • przyciski „<” i „>” zawijają w obie strony (użyj operatora %, indeks od 0),
  • pod zdjęciem jest 5 kropek <span class="kropka"> wygenerowanych pętlą; aktywna kropka ma klasę aktywna; kliknięcie kropki przeskakuje do danego zdjęcia,
  • strzałki klawiatury ArrowLeft / ArrowRight działają jak przyciski (zdarzenie keydown na document),
  • akapit pokazuje „Zdjęcie 3 z 5”.
Pokaż rozwiązanie
Rozwiązanie: galeria z kropkami
function adresObrazka(numer) {
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="120">' +
    '<rect width="240" height="120" fill="hsl(' + numer * 61 + ' 55% 45%)"/>' +
    '<text x="120" y="75" font-size="34" fill="white" text-anchor="middle" font-family="system-ui">' +
    numer + '.jpg</text></svg>';
  return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}

const zdjecia = [1, 2, 3, 4, 5].map(adresObrazka); // 5 adresów
let indeks = 0; // stan

const zdjecie = document.getElementById('zdjecie');
const kropki = document.getElementById('kropki');

// kropki generowane pętlą – każda zna swój numer przez data-indeks
for (let i = 0; i < zdjecia.length; i++) {
  const kropka = document.createElement('span');
  kropka.className = 'kropka';
  kropka.dataset.indeks = i;
  kropka.addEventListener('click', (e) => {
    indeks = Number(e.target.dataset.indeks); // dataset zwraca napis
    odswiez();
  });
  kropki.appendChild(kropka);
}

function odswiez() {
  zdjecie.src = zdjecia[indeks];
  zdjecie.alt = 'Zdjęcie ' + (indeks + 1);
  document.getElementById('opis').textContent = 'Zdjęcie ' + (indeks + 1) + ' z ' + zdjecia.length;
  const wszystkie = document.querySelectorAll('.kropka');
  for (const k of wszystkie) k.classList.remove('aktywna');   // wszystkim: nieaktywna
  wszystkie[indeks].classList.add('aktywna');                 // jednej: aktywna
}

function nastepne() {
  indeks = (indeks + 1) % zdjecia.length;
  odswiez();
}
function poprzednie() {
  indeks = (indeks - 1 + zdjecia.length) % zdjecia.length;
  odswiez();
}

document.getElementById('dalej').addEventListener('click', nastepne);
document.getElementById('wstecz').addEventListener('click', poprzednie);
document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowRight') nastepne();
  if (e.key === 'ArrowLeft') poprzednie();
});

odswiez();
Podgląd

Kliknij najpierw w podgląd, żeby ramka miała fokus – dopiero wtedy odbiera klawisze.

Zadanie 4
trudnejak na INF.03

Wypożyczalnia kajaków: cena zależna od wyświetlonego modelu

Strona cennik.html zawiera (HTML jest gotowy – twoje zadanie to tylko skrypt):

cennik.html (fragment)
<section id="modele">
  <img class="mini" src="kajak1m.jpg" alt="Kajak jednoosobowy" onclick="wybierzModel(1)">
  <img class="mini" src="kajak2m.jpg" alt="Kajak dwuosobowy" onclick="wybierzModel(2)">
</section>
<img id="duzy" src="kajak1d.jpg" alt="Kajak jednoosobowy">
<label>Liczba godzin: <input type="number" id="godziny" min="1" value="1"></label>
<button onclick="obliczKoszt()">Oblicz</button>
<p id="koszt"></p>

Skrypt, wykonywany po stronie klienta, działa tak:

  1. Kliknięcie miniatury zmienia obraz duzy na kajak1d.jpg albo kajak2d.jpg oraz jego tekst alternatywny.
  2. Kliknięcie „Oblicz” pobiera liczbę godzin i sprawdza, który model jest aktualnie wyświetlony: jednoosobowy kosztuje 30 zł/h, dwuosobowy 45 zł/h. Obliczenie ma działać także dla stanu początkowego (jednoosobowy).
  3. W akapicie pod przyciskiem pojawia się tekst: „Kajak jednoosobowy, 3 h: 90 zł” (nazwa modelu z tekstu alternatywnego, liczba godzin, koszt).
  4. Stosuj znaczące nazwy zmiennych i funkcji w języku polskim lub angielskim.

Kryteria oceny w stylu „Zasad oceniania” (każde 0 lub 1):

  • 5.1 Skrypt napisany w JavaScript, zawiera co najmniej jedną poprawną składniowo instrukcję.
  • 5.2 Kliknięcie miniatury zmienia atrybut src dużego obrazu na właściwy plik …d.jpg.
  • 5.3 Skrypt pobiera liczbę godzin z pola edycyjnego.
  • 5.4 Koszt liczony jest wg stawki modelu aktualnie wyświetlonego – sprawdzane dla obu modeli oraz dla stanu początkowego.
  • 5.5 Wynik wyświetlony pod przyciskiem w akapicie, w formacie „<model>, <godziny> h: <koszt> zł” – sprawdzane dla dwóch różnych liczb godzin.
  • 5.6 Użyto co najmniej jednej zmiennej lub funkcji o znaczącej nazwie.
Pokaż rozwiązanie

W placu zabaw brak plików graficznych, więc obrazy są rysowane funkcją rysunek; na egzaminie zamiast niej wpisujesz nazwy plików ('kajak' + numer + 'd.jpg'). Logika odczytu stanu jest identyczna.

Rozwiązanie: wypożyczalnia kajaków
// zamiennik plików kajak1m.jpg, kajak2m.jpg, kajak1d.jpg, kajak2d.jpg
function rysunek(numer, w, h) {
  const kolor = numer === 1 ? 'SeaGreen' : 'SteelBlue';
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '">' +
    '<ellipse cx="' + w / 2 + '" cy="' + h / 2 + '" rx="' + (w / 2 - 6) + '" ry="' + (h / 4) + '" fill="' + kolor + '"/>' +
    '<text x="' + w / 2 + '" y="' + (h / 2 + 6) + '" font-size="' + h / 4 + '" fill="white" text-anchor="middle" font-family="system-ui">kajak' + numer + '</text></svg>';
  return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}
for (const mini of document.querySelectorAll('.mini')) mini.src = rysunek(Number(mini.dataset.model), 70, 40);

const STAWKI = { 1: 30, 2: 45 };                // zł za godzinę wg modelu
const NAZWY = { 1: 'Kajak jednoosobowy', 2: 'Kajak dwuosobowy' };
const duzy = document.getElementById('duzy');
duzy.src = rysunek(1, 210, 120);                // stan początkowy: model 1

function wybierzModel(numer) {
  duzy.src = rysunek(numer, 210, 120);          // na egzaminie: 'kajak' + numer + 'd.jpg'
  duzy.alt = NAZWY[numer];
  duzy.dataset.model = numer;                    // stan zapisany w DOM
}

function obliczKoszt() {
  const godziny = parseInt(document.getElementById('godziny').value);
  const model = Number(duzy.dataset.model);      // który model jest TERAZ wyświetlony
  // wersja egzaminacyjna z nazwami plików: const model = duzy.src.includes('kajak2') ? 2 : 1;
  const koszt = godziny * STAWKI[model];
  document.getElementById('koszt').textContent =
    duzy.alt + ', ' + godziny + ' h: ' + koszt + ' zł';
}
Podgląd

Samoocena: kliknij „Oblicz” bez wybierania modelu (stan początkowy → 30 zł/h), potem wybierz model 2 i policz dla 2 oraz 5 godzin. Sprawdź tekst co do przecinka i spacji.

Test powtórkowy

Test powtórkowy

Format jak w części pisemnej INF.03: 4 odpowiedzi, jedna poprawna.

0/0poprawnych
Sprawdź się

Co wypisze ten kod po trzech wywołaniach?

var licznik = 0;
function klik() {
licznik = licznik + 1;
document.write(licznik + ' ');
}
klik(); klik(); klik();
Sprawdź się

Galeria ma zdjęcia 1.jpg–5.jpg. Który fragment poprawnie zawija numer po kliknięciu „>”?

Sprawdź się

Jaką wartość ma indeks po wykonaniu kodu?

var indeks = 0;
var n = 6;
indeks = (indeks - 1 + n) % n;
Sprawdź się

Pasek postępu startuje od 0 % i przy każdym zdarzeniu blur wykonuje szerokosc = Math.min(szerokosc + 25, 100). Jaką szerokość ma po 6 zdarzeniach?

Sprawdź się

Który zapis w atrybucie HTML przekaże funkcji informację, który z trzech przycisków kliknięto?

Sprawdź się

Co jest prawdą o tym kodzie, jeśli w arkuszu CSS jest reguła #box { display: none; }, a w HTML brak atrybutu style?

var box = document.getElementById('box');
if (box.style.display == 'none') {
box.style.display = 'block';
}
Sprawdź się

Tablica cytaty ma 3 elementy, i = 2. Po i = (i + 1) % cytaty.length; instrukcja p.innerHTML = cytaty[i]; wyświetli:

Sprawdź się

W zakładkach po trzech kliknięciach różnych przycisków widoczne są wszystkie trzy sekcje jednocześnie. Jaka jest najbardziej prawdopodobna przyczyna?

Sprawdź się

Duży obraz ma w HTML src='1d.bmp'. Który warunek niezawodnie sprawdzi, że właśnie on jest wyświetlony?

Sprawdź się

Po załadowaniu strony widać cytat 1. Użytkownik klika kolejno: cytat 1, potem widoczny cytat, potem znów widoczny cytat. Co zobaczy po trzech kliknięciach?

function dalej(nr) {
var nastepny = nr + 1;
if (nastepny > 3) nastepny = 1;
document.getElementById('c' + nr).style.display = 'none';
document.getElementById('c' + nastepny).style.display = 'block';
}