📖 Подробное описание работы «Аккордеон-спойлера»
🎯 Назначение
Скрипт автоматически находит мини-профили пользователей в топиках форума и сворачивает менее важные поля (IP, возраст, город, кол-во сообщений и т.д.) в компактный аккордеон. Пользователь видит только основные поля (аватар, ник, статус), а всё остальное раскрывается по клику на кнопку со стрелкой.
Ключевое отличие от аналогов: код написан на чистом vanilla JavaScript без использования jQuery или других библиотек. Это означает:
✅ Минимальный размер — нет лишних килобайт зависимостей
✅ Высокая производительность — прямая работа с DOM через нативные API
✅ Совместимость — работает в любом современном браузере без дополнительных плагинов
✅ Современные подходы — используются querySelectorAll, MutationObserver, classList, CSS Grid и другие актуальные стандарты
👀 Как это выглядит
[html]<img class="postimg" loading="lazy" src="https://upforme.ru/uploads/001a/f0/7d/2/580448.webp" alt="https://upforme.ru/uploads/001a/f0/7d/2/580448.webp">[/html]
[html]<img class="postimg" loading="lazy" src="https://upforme.ru/uploads/001a/f0/7d/2/108643.webp" alt="https://upforme.ru/uploads/001a/f0/7d/2/108643.webp">[/html]
🧱 Структура, которую создаёт скрипт
Для каждого мини-профиля (.post-author) скрипт генерирует такую DOM-структуру:
.post-author
├── .pa-avatar ← остаётся на месте (SHOW)
├── .pa-author ← остаётся на месте (SHOW)
├── .pa-title ← остаётся на месте (SHOW)
│
├── .msp-wrap ← ← ← НОВЫЙ контейнер (вставляется сюда)
│ ├── .msp-toggle ← кнопка-стрелка с тултипом
│ │ └── <img> ← иконка стрелки (поворачивается на 180°)
│ │
│ └── .msp-spoiler ← grid-обёртка (анимирует высоту)
│ └── .msp-spoiler-inner ← реальный контейнер контента
│ ├── .pa-ip ← перенесено внутрь (HIDE)
│ ├── .pa-age ← перенесено внутрь (HIDE)
│ ├── .pa-from ← перенесено внутрь (HIDE)
│ ├── .pa-posts ← перенесено внутрь (HIDE)
│ ├── .pa-respect ← перенесено внутрь (HIDE)
│ ├── .pa-sex ← перенесено внутрь (HIDE)
│ └── .pa-ua ← перенесено внутрь (HIDE)
⚙️ Как работает JavaScript
1. Конфигурация — что показывать, что скрывать
var SHOW = ['pa-author','pa-title','pa-avatar'];
var HIDE = ['pa-ip','pa-age','pa-from','pa-posts','pa-respect','pa-sex','pa-ua'];
SHOW — поля, которые всегда видны. Скрипт ищет последнее из них, чтобы вставить спойлер сразу после.
HIDE — поля, которые будут «упакованы» внутрь спойлера.
2. Инициализация — поиск и перенос элементов
document.querySelectorAll('.post-author').forEach(function (box) {
Для каждого мини-профиля:
Проверка box.dataset.mspDone — чтобы не обработать один и тот же блок дважды.
Сбор скрытых полей — скрипт перебирает массив HIDE, находит соответствующие элементы внутри .post-author и складывает в массив hidden.
Создание DOM-структуры — создаются .msp-wrap, .msp-toggle, .msp-spoiler, .msp-spoiler-inner.
Перенос элементов — все найденные скрытые поля физически перемещаются внутрь .msp-spoiler-inner через appendChild() (браузер сам удаляет элемент со старого места).
Вставка спойлера — скрипт ищет последний видимый элемент из SHOW и вставляет .msp-wrap сразу после него через insertBefore(wrap, anchor.nextSibling).
3. Обработчик клика
btn.addEventListener('click', function () {
wrap.classList.toggle('msp-open');
});При клике просто добавляется/убирается класс .msp-open на обёртке. Вся анимация — на CSS.
4. Защита от динамической подгрузкиnew MutationObserver(init).observe(document.body, { childList: true, subtree: true });
MutationObserver следит за изменениями DOM. Если форум подгружает новые сообщения через AJAX (бесконечная прокрутка, переход по страницам без перезагрузки), скрипт автоматически обработает новые мини-профили.
🎨 Как работает CSS-анимация (Grid-трюк)
Это самая интересная часть — анимация высоты без JavaScript.
Проблема обычной анимации
Чтобы анимировать height: 0 → auto, нужно знать точную высоту контента в пикселях. Но высота зависит от контента, шрифтов, адаптивности... Раньше это решали так:// ❌ Старый подход: JS считает высоту
element.style.height = element.scrollHeight + 'px';Это требует пересчёта при каждом изменении контента, при ресайзе окна, и работает дёрганно.
Решение через CSS Grid
.msp-spoiler {
display: grid;
grid-template-rows: 0fr; /* ← свёрнуто */
transition: grid-template-rows 0.4s ease-out;
}
.msp-wrap.msp-open .msp-spoiler {
grid-template-rows: 1fr; /* ← развёрнуто */
}
.msp-spoiler > .msp-spoiler-inner {
overflow: hidden;
min-height: 0; /* ← обязательно! */
}Пошаговая механика
1. msp-spoiler — это grid-контейнер с одной строкой
2. grid-template-rows: 0fr — высота строки = 0 долей от доступного пространства. Фактически 0px.
3. grid-template-rows: 1fr — высота строки = 1 доля = весь доступный контент. Браузер сам вычисляет нужную высоту.
4. transition плавно интерполирует между 0fr и 1fr за 0.4 секунды
5. msp-spoiler-inner с overflow: hidden обрезает контент, который вылезает за пределы grid-ячейки
6. min-height: 0 — критически важно! Без этого grid-элемент не может стать меньше своего минимального контента (по спецификации CSS grid min-height: auto по умолчанию), и анимация не сожмётся до нуляПочему это работает
0fr → 0.1fr → 0.3fr → 0.6fr → 0.9fr → 1fr
0px 12px 38px 72px 108px 120px (пример)Браузер линейно интерполирует значение fr от 0 до 1, а высота grid-ячейки плавно растёт. Внутренний overflow: hidden обрезает контент на каждом кадре — получается плавное «раскрытие».
Поддержка браузерами - Chrome, Firefox, Safari, Edge поддерживают CSS-анимацию с 2023 года.
На момент 2026 года это работает во всех актуальных браузерах.
🖱️ Интерактивные элементы
Кнопка-стрелка
.msp-toggle img {
transition: transform 0.3s;
}
.msp-wrap.msp-open .msp-toggle img {
transform: rotate(180deg); /* стрелка переворачивается */
}Стрелка плавно поворачивается на 180° при раскрытии — визуальная подсказка состояния.
Тултип (всплывающая подсказка) при наведении
.msp-toggle::after {
content: attr(data-tip); /* берёт текст из data-tip="Развернуть/Свернуть" */
position: absolute;
bottom: 100%;
opacity: 0;
transition: opacity 0.2s;
}
.msp-toggle:hover::after { opacity: 1; }Псевдоэлемент ::after создаёт всплывающую подсказку над кнопкой. Текст берётся из атрибута data-tip — удобно менять без правки CSS.
🔄 Полный цикл работы
Страница загружается (или подгружается через AJAX)
▼
init() находит все .post-author на странице
▼
Для каждого: проверяет dataset.mspDone
(уже обработан? → пропускаем)
▼
Собирает поля из массива HIDE
(pa-ip, pa-age, pa-from и т.д.)
▼
Создаёт DOM: .msp-wrap > .msp-toggle + .msp-spoiler
Переносит скрытые поля внутрь .msp-spoiler-inner
▼
Вставляет .msp-wrap после последнего поля из SHOW
▼
Пользователь кликает на .msp-toggle
→ toggle('msp-open')
→ CSS анимирует grid-template-rows: 0fr ↔ 1fr
→ Стрелка поворачивается на 180°
💡 Ключевые преимущества этого подхода
Без JS-расчётов - не нужно измерять scrollHeight, не нужно пересчитывать при ресайзе
Плавная анимация - CSS transition работает на GPU, 60fps без лагов
Адаптивность - высота подстраивается под любой контент автоматически
Производительность - один MutationObserver на всю страницу, а не слушатели на каждом элементе
Защита от повторов - dataset.mspDone гарантирует, что каждый блок обработан ровно один раз
Совместимость с AJAX - MutationObserver ловит динамически подгруженные сообщения
🔧 Как настроить под себя
// Какие поля оставить видимыми (CSS-классы без точки)
var SHOW = ['pa-author', 'pa-title', 'pa-avatar'];// Какие поля спрятать в спойлер
var HIDE = ['pa-ip', 'pa-age', 'pa-from', 'pa-posts', 'pa-respect', 'pa-sex', 'pa-ua'];// Текст всплывающей подсказки
var TIP = 'Развернуть/Свернуть';// URL иконки-стрелки (32×16 px)
var IMG = 'https://forumstatic.ru/files/001a/f0/7d/19517.png';Достаточно поменять массивы SHOW / HIDE — и скрипт автоматически перераспределит поля. Порядок в SHOW определяет, после какого поля появится кнопка спойлера (скрипт берёт последний найденный).
📝 Исходный код
Код:<!--НАЧАЛО html-верх - Спойлер для скрытия полей мини-профиля в топиках--> <style> .msp-wrap { display: block; margin: 2px 0; } .msp-toggle { display: inline-flex; align-items: center; cursor: pointer; padding: 0 6px; margin: 2px 0; border-radius: 4px; background: rgba(0,0,0,0.05); position: relative; vertical-align: middle; transition: background 0.2s; } .msp-toggle:hover { background: rgba(0,0,0,0.1); } .msp-toggle img { width: 32px; height: 16px; display: block; transition: transform 0.3s; } .msp-wrap.msp-open .msp-toggle img { transform: rotate(180deg); } /* Магия grid-анимации */ .msp-spoiler { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.4s ease-out; } .msp-wrap.msp-open .msp-spoiler { grid-template-rows: 1fr; } .msp-spoiler > .msp-spoiler-inner { overflow: hidden; min-height: 0; } .msp-toggle::after { content: attr(data-tip); position: absolute; bottom: 100%; left: 0; transform: translateY(-4px); background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 1000; margin-bottom: 4px; } </style> <script> (function () { 'use strict'; var SHOW = ['pa-author','pa-title','pa-avatar']; var HIDE = ['pa-ip','pa-age','pa-from','pa-posts','pa-respect','pa-sex','pa-ua']; var TIP = 'Развернуть/Свернуть'; var IMG = 'https://forumstatic.ru/files/001a/f0/7d/19517.png'; function init() { document.querySelectorAll('.post-author').forEach(function (box) { if (box.dataset.mspDone) return; box.dataset.mspDone = '1'; var hidden = []; HIDE.forEach(function (c) { var el = box.querySelector('.' + c); if (el) hidden.push(el); }); if (!hidden.length) return; var wrap = document.createElement('div'); wrap.className = 'msp-wrap'; var btn = document.createElement('div'); btn.className = 'msp-toggle'; btn.setAttribute('data-tip', TIP); btn.innerHTML = '<img src="' + IMG + '" alt="">'; var spoiler = document.createElement('div'); spoiler.className = 'msp-spoiler'; // Внутренняя обёртка обязательна для grid-трюка var inner = document.createElement('div'); inner.className = 'msp-spoiler-inner'; hidden.forEach(function (el) { inner.appendChild(el); }); spoiler.appendChild(inner); wrap.appendChild(btn); wrap.appendChild(spoiler); var anchor = null; for (var i = SHOW.length - 1; i >= 0; i--) { anchor = box.querySelector('.' + SHOW[i]); if (anchor) break; } if (anchor && anchor.parentNode) { anchor.parentNode.insertBefore(wrap, anchor.nextSibling); } else { box.appendChild(wrap); } btn.addEventListener('click', function () { wrap.classList.toggle('msp-open'); }); }); } document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); new MutationObserver(init).observe(document.body, { childList: true, subtree: true }); })(); </script> <!-- КОНЕЦ Спойлер для скрытия полей мини-профиля в топиках--->
P.S. Пример реального кода на моём форуме, с небольшими добавлениями (с дополнительными полями):
Код:<!--НАЧАЛО html-верх - Спойлер для скрытия полей мини-профиля в топиках--> <style> .msp-wrap { display: block; margin: 2px 0; } .msp-toggle { display: inline-flex; align-items: center; cursor: pointer; padding: 0 6px; margin: 2px 0; border-radius: 4px; background: rgba(0,0,0,0.05); position: relative; vertical-align: middle; transition: background 0.2s; } .msp-toggle:hover { background: rgba(0,0,0,0.1); } .msp-toggle img { width: 32px; height: 16px; display: block; transition: transform 0.3s; } .msp-wrap.msp-open .msp-toggle img { transform: rotate(180deg); } .msp-toggle::after { content: attr(data-tip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 10; } .msp-toggle:hover::after { opacity: 1; } /* Магия grid-анимации */ .msp-spoiler { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.4s ease-out; } .msp-wrap.msp-open .msp-spoiler { grid-template-rows: 1fr; } .msp-spoiler > .msp-spoiler-inner { overflow: hidden; min-height: 0; } .msp-toggle::after { content: attr(data-tip); position: absolute; bottom: 100%; left: 0; /* Вместо left: 50% */ transform: translateY(-4px); /* Убираем translateX */ background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 1000; /* Поверх всего */ margin-bottom: 4px; } </style> <script> (function () { 'use strict'; var SHOW = ['pa-author','pa-title','pa-fld1','pa-avatar']; var HIDE = ['pa-ip','pa-age','pa-from','pa-posts','pa-respect','pa-sex','pa-fld2','pa-ua']; var TIP = 'Развернуть/Свернуть'; var IMG = 'https://forumstatic.ru/files/001a/f0/7d/19517.png'; function init() { document.querySelectorAll('.post-author').forEach(function (box) { if (box.dataset.mspDone) return; box.dataset.mspDone = '1'; var hidden = []; HIDE.forEach(function (c) { var el = box.querySelector('.' + c); if (el) hidden.push(el); }); if (!hidden.length) return; var wrap = document.createElement('div'); wrap.className = 'msp-wrap'; var btn = document.createElement('div'); btn.className = 'msp-toggle'; btn.setAttribute('data-tip', TIP); btn.innerHTML = '<img src="' + IMG + '" alt="">'; var spoiler = document.createElement('div'); spoiler.className = 'msp-spoiler'; // Внутренняя обёртка обязательна для grid-трюка var inner = document.createElement('div'); inner.className = 'msp-spoiler-inner'; hidden.forEach(function (el) { inner.appendChild(el); }); spoiler.appendChild(inner); wrap.appendChild(btn); wrap.appendChild(spoiler); var anchor = null; for (var i = SHOW.length - 1; i >= 0; i--) { anchor = box.querySelector('.' + SHOW[i]); if (anchor) break; } if (anchor && anchor.parentNode) { anchor.parentNode.insertBefore(wrap, anchor.nextSibling); } else { box.appendChild(wrap); } btn.addEventListener('click', function () { wrap.classList.toggle('msp-open'); }); }); } document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); new MutationObserver(init).observe(document.body, { childList: true, subtree: true }); })(); </script> <!-- КОНЕЦ Спойлер для скрытия полей мини-профиля в топиках--->


