Урок 10

Практика: интерактивные компоненты

Модальное окно

<button id="open">Открыть</button>
<div class="modal" id="modal" hidden>
    <div class="modal-content">
        <button class="close">&times;</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
  • Делегирование для динамических элементов