Krok 10: dwa niezależne kalendarze
Kalendarz: logika oddzielona od widoku - Kalendarz w JavaScript krok po kroku (HTML/CSS/JS)
Nagroda za rozdzielenie: drugi kalendarz to dwie linie. Każdy widok ma własny model, więc przełączanie i zaznaczanie w jednym nie rusza drugiego. Ta sama klasa modelu mogłaby obsłużyć też widok tekstowy z konsoli.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="utf-8">
<title>Kalendarze</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>
<div id="main"></div>
<div id="second"></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;
}
}
// widok: buduje pasek i siatkę w podanym elemencie, zdarzenia zmieniają model
class CalendarView {
constructor(root, calendar) {
this.calendar = calendar;
root.innerHTML = `
<p class="bar">
<button class="prev"><</button> <strong class="title"></strong> <button class="next">></button>
</p>
<div class="grid"></div>`;
this.title = root.querySelector('.title');
this.grid = root.querySelector('.grid');
root.querySelector('.prev').addEventListener('click', () => this.update(() => calendar.move(-1)));
root.querySelector('.next').addEventListener('click', () => this.update(() => calendar.move(1)));
this.grid.addEventListener('click', e => {
const div = e.target.closest('.day');
if (div) this.update(() => calendar.select(Number(div.dataset.day)));
});
this.render();
}
update(change) {
change();
this.render();
}
render() {
this.title.textContent = this.calendar.title;
renderGrid(this.grid, this.calendar.weeks(), this.calendar.selected);
}
}
const today = new Date();
new CalendarView(document.getElementById('main'), new Calendar(today.getFullYear(), today.getMonth() + 1, today));
const next = new Calendar(today.getFullYear(), today.getMonth() + 1, today);
next.move(1);
new CalendarView(document.getElementById('second'), next);
</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.