Практика: интерактивные компоненты
Модальное окно
<button id="open">Открыть</button>
<div class="modal" id="modal" hidden>
<div class="modal-content">
<button class="close">×</button>
<p>Содержимое</p>
</div>
</div>
const modal = document.getElementById('modal');
const openBtn = document.getElementById('open');
function openModal() {
modal.hidden = false;
document.body.style.overflow = 'hidden';
}
function closeModal() {
modal.hidden = true;
document.body.style.overflow = '';
}
openBtn.addEventListener('click', openModal);
modal.addEventListener('click', (event) => {
if (event.target === modal) closeModal();
});
modal.querySelector('.close').addEventListener('click', closeModal);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !modal.hidden) closeModal();
});
Табы
<div class="tabs">
<button class="tab active" data-tab="1">Первый</button>
<button class="tab" data-tab="2">Второй</button>
</div>
<div class="panel active" data-panel="1">Содержимое 1</div>
<div class="panel" data-panel="2">Содержимое 2</div>
document.querySelector('.tabs').addEventListener('click', (event) => {
const tab = event.target.closest('.tab');
if (!tab) return;
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active'));
tab.classList.add('active');
document.querySelector(`[data-panel="${tab.dataset.tab}"]`).classList.add('active');
});
Аккордеон
document.querySelectorAll('.accordion-header').forEach((header) => {
header.addEventListener('click', () => {
const item = header.parentElement;
const isOpen = item.classList.contains('open');
document.querySelectorAll('.accordion-item').forEach((i) => {
i.classList.remove('open');
});
if (!isOpen) item.classList.add('open');
});
});
Выпадающее меню
const dropdown = document.querySelector('.dropdown');
dropdown.querySelector('.trigger').addEventListener('click', (event) => {
event.stopPropagation();
dropdown.classList.toggle('open');
});
document.addEventListener('click', () => {
dropdown.classList.remove('open');
});
Уведомления
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.append(notification);
setTimeout(() => {
notification.classList.add('hiding');
notification.addEventListener('transitionend', () => notification.remove());
}, 3000);
}
Слайдер
let current = 0;
const slides = document.querySelectorAll('.slide');
function show(index) {
slides.forEach((s, i) => {
s.classList.toggle('active', i === index);
});
current = index;
}
document.querySelector('.next').addEventListener('click', () => {
show((current + 1) % slides.length);
});
document.querySelector('.prev').addEventListener('click', () => {
show((current - 1 + slides.length) % slides.length);
});
Ленивая загрузка изображений
<img data-src="image.jpg" alt="">
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));
To-Do список
const form = document.querySelector('.todo-form');
const list = document.querySelector('.todo-list');
const input = form.querySelector('input');
form.addEventListener('submit', (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
const li = document.createElement('li');
li.innerHTML = `
<input type="checkbox">
<span></span>
<button class="delete">×</button>
`;
li.querySelector('span').textContent = text;
list.append(li);
input.value = '';
});
list.addEventListener('click', (event) => {
if (event.target.matches('.delete')) {
event.target.closest('li').remove();
}
if (event.target.matches('input[type="checkbox"]')) {
event.target.closest('li').classList.toggle('done', event.target.checked);
}
});
Бесконечный скролл
let loading = false;
window.addEventListener('scroll', async () => {
if (loading) return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 200) {
loading = true;
await loadMore();
loading = false;
}
});
Итоги
- Модалка с Escape и кликом по фону
- Табы через data-атрибуты
- Аккордеон с закрытием остальных
- Dropdown с закрытием по клику вне
- Уведомления с автоудалением
- Ленивая загрузка через IntersectionObserver
- Делегирование для динамических элементов