Krok 8: klasa Calendar jako model

Kalendarz: logika oddzielona od widoku - Kalendarz w JavaScript krok po kroku (HTML/CSS/JS)

Stan i operacje na nim łączy klasa `Calendar`: pola `year`, `month`, `selected`, getter `title`, metody `weeks()`, `move(delta)` i `select(day)`. Klasa nie dotyka strony. Zdarzenia wołają metodę modelu i przerysowują widok; zmiana miesiąca kasuje tu zaznaczenie (to decyzja modelu, nie widoku).

<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="utf-8">
  <title>Kalendarz</title>
  <style>
    body { font-family: sans-serif; max-width: 36em; line-height: 1.4; }
    input, select, button, textarea { font: inherit; padding: 0.2em 0.4em; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #94a3b8; padding: 0.2em 0.6em; }

    .grid { display: grid; grid-template-columns: repeat(7, 40px); gap: 6px; margin-bottom: 1em; }
    .grid div { height: 40px; line-height: 40px; text-align: center; background: lightgrey; }
    .grid .day-name { background: darkgrey; color: white; }
    .grid .weekend { background: lightblue; color: white; }
    .grid .none { background: white; }
    .grid .today { background: crimson; color: white; }
    .grid .day { cursor: pointer; }
    .grid .selected { outline: 3px solid #f59e0b; }
    .bar { display: flex; align-items: center; gap: 0.5em; width: 316px; }
    .bar strong { flex: 1; text-align: center; }
    table.cal td, table.cal th { width: 2.2em; text-align: center; }
  </style>
</head>
<body>
  <p class="bar">
    <button id="prev">&lt;</button>
    <strong id="title"></strong>
    <button id="next">&gt;</button>
  </p>
  <div id="calendar" class="grid"></div>
  <script>
    const MONTHS = ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec',
      'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'];
    const DAY_NAMES = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'];
    const sameDay = (a, b) =>
      a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();

    // model: miesiąc jako tablica tygodni, dzień = obiekt z informacjami, puste pole = null
    function monthWeeks(year, month, today) {
      const firstDay = (new Date(year, month - 1, 1).getDay() + 6) % 7;
      const days = new Date(year, month, 0).getDate();
      const cells = Array(firstDay).fill(null);
      for (let d = 1; d <= days; d++) {
        cells.push({
          day: d,
          weekend: (firstDay + d - 1) % 7 >= 5,
          today: sameDay(new Date(year, month - 1, d), today),
        });
      }
      while (cells.length % 7 !== 0) cells.push(null);
      const weeks = [];
      for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
      return weeks;
    }
    function cell(text, ...classes) {
      const div = document.createElement('div');
      div.textContent = text;
      div.classList.add(...classes.filter(Boolean));
      return div;
    }
    // widok: siatka z modelu, nie liczy już żadnych dat
    function renderGrid(root, weeks, selectedDay = null) {
      const fragment = document.createDocumentFragment();
      DAY_NAMES.forEach((name, k) => fragment.append(cell(name, 'day-name', k >= 5 ? 'weekend' : '')));
      for (const week of weeks) {
        for (const c of week) {
          if (c === null) {
            fragment.append(cell('', 'none'));
            continue;
          }
          const div = cell(c.day, 'day', c.today ? 'today' : c.weekend ? 'weekend' : '', c.day === selectedDay ? 'selected' : '');
          div.dataset.day = c.day;
          fragment.append(div);
        }
      }
      root.replaceChildren(fragment);
    }
    // model: stan kalendarza i operacje na nim, bez dostępu do strony
    class Calendar {
      constructor(year, month, today) {
        this.year = year;
        this.month = month;
        this.today = today;
        this.selected = null;
      }

      get title() {
        return `${MONTHS[this.month - 1]} ${this.year}`;
      }

      weeks() {
        return monthWeeks(this.year, this.month, this.today);
      }

      move(delta) {
        const index = this.year * 12 + (this.month - 1) + delta;
        this.year = Math.floor(index / 12);
        this.month = (index % 12) + 1;
        this.selected = null;
      }

      select(day) {
        this.selected = day;
      }
    }
    const calendar = new Calendar(new Date().getFullYear(), new Date().getMonth() + 1, new Date());

    function render() {
      document.getElementById('title').textContent = calendar.title;
      renderGrid(document.getElementById('calendar'), calendar.weeks(), calendar.selected);
    }

    document.getElementById('prev').addEventListener('click', () => {
      calendar.move(-1);
      render();
    });
    document.getElementById('next').addEventListener('click', () => {
      calendar.move(1);
      render();
    });
    document.getElementById('calendar').addEventListener('click', e => {
      const div = e.target.closest('.day');
      if (!div) return;
      calendar.select(Number(div.dataset.day));
      render();
    });
    render();
  </script>
</body>
</html>

Ładowanie interaktywnego edytora... Jeśli się nie pojawi, włącz JavaScript.

Wszystkie lekcje kursu Kalendarz w JavaScript krok po kroku·Zaloguj się, aby zapisać postęp na koncie.